Orion.Abstractions
0.3.0
See the version list below for details.
dotnet add package Orion.Abstractions --version 0.3.0
NuGet\Install-Package Orion.Abstractions -Version 0.3.0
<PackageReference Include="Orion.Abstractions" Version="0.3.0" />
<PackageVersion Include="Orion.Abstractions" Version="0.3.0" />
<PackageReference Include="Orion.Abstractions" />
paket add Orion.Abstractions --version 0.3.0
#r "nuget: Orion.Abstractions, 0.3.0"
#:package Orion.Abstractions@0.3.0
#addin nuget:?package=Orion.Abstractions&version=0.3.0
#tool nuget:?package=Orion.Abstractions&version=0.3.0
<p align="center"> <img src="docs/logo.png" alt="Orion.Abstractions" width="150" /> </p>
Orion.Abstractions
Shared foundation primitives for the Orion family of .NET libraries. Three primitives kept being re-implemented (and kept drifting) across the family: fault-safe observer invocation, OpenTelemetry instrumentation conventions, and a testable clock. They now live here, once, correctly. The package has no Orion dependencies of its own, so any library can depend on it to inherit the Orion conventions.
Features
- Fault-safe observer invocation (
SafeObserverInvoker) - a null observer is a no-op, observer faults are swallowed so an observability outage cannot break the load-bearing path, andOperationCanceledExceptionalways propagates on cancellation. Includes a resolve-inside-the-guard variant so a throwing observer constructor cannot abort the host path at resolution time. - OpenTelemetry conventions (
OrionInstrumentation) - a base class that pairs a consistently namedActivitySourceandMeter, plus a static-tag stamping pattern for multi-tenant / multi-region dashboard splitting without a secondMeter. - Instance-scoped instrumentation (
OrionInstrumentation) - an instance can opt into a per-instance scope id and extra Meter-level tags, so itsMetercarries anorion.instancetag (plus any custom tags) for per-instance metric partitioning.OrionInstrumentation.ListensTothen filters aMeterListenerto exactly one instance's instruments, even when several live instances share the same Meter name. - Testable clock (
IOrionClock/SystemOrionClock) - a thin seam overTimeProviderso every Orion background worker, lease, and scheduler shares one clock contract and one DI registration. - Deterministic test clock (
FrozenOrionClock, inOrion.Abstractions.Testing) - a frozen, advanceable clock for testing lease expiry, grace periods, and scheduled work without real delays. - One-line DI registration (
AddOrionAbstractions) - registers the production clock viaTryAdd, so it is safe to call from multiple Orion packages and a consumer override always wins. - No dependencies beyond
Microsoft.Extensions.DependencyInjection.Abstractions; multi-targetsnet8.0,net9.0, andnet10.0; nullable enabled and warnings-as-errors.
Install
dotnet add package Orion.Abstractions
# Optional: the testing companion (FrozenOrionClock), reference from your test project only
dotnet add package Orion.Abstractions.Testing
Quick start
using Microsoft.Extensions.DependencyInjection;
using Moongazing.Orion.Abstractions;
using Moongazing.Orion.Abstractions.Time;
var services = new ServiceCollection();
services.AddOrionAbstractions(); // registers IOrionClock -> SystemOrionClock (TryAdd)
using var provider = services.BuildServiceProvider();
var clock = provider.GetRequiredService<IOrionClock>();
DateTimeOffset now = clock.UtcNow;
long start = clock.GetTimestamp();
// ... do work ...
TimeSpan elapsed = clock.GetElapsedTime(start);
Usage
Fault-safe observer invocation
Route every consumer-supplied observer hook through SafeObserverInvoker. A null observer is skipped, a faulting observer is swallowed (and optionally reported), and cancellation is never downgraded to a swallowed warning.
using Moongazing.Orion.Abstractions.Observers;
// Synchronous: a null observer is a no-op; a fault is swallowed and reported.
SafeObserverInvoker.Invoke(observer, o => o.OnSomething(payload),
onFault: ex => logger.LogWarning(ex, "observer faulted; host continued"));
// Asynchronous: OperationCanceledException propagates when the token is cancelled.
await SafeObserverInvoker.InvokeAsync(observer,
o => o.OnSomethingAsync(payload),
onFault: ex => logger.LogWarning(ex, "observer faulted"),
cancellationToken: ct);
// Resolution itself inside the guard: a throwing observer ctor cannot abort the host path.
SafeObserverInvoker.Resolve(
() => serviceProvider.GetService<IMyObserver>(),
o => o.OnSomething(payload),
onFault: ex => logger.LogWarning(ex, "observer resolution faulted"));
What an observer may and may not do (no throwing from onFault, no blocking the host, cancellation semantics) is spelled out in the normative observer contract. The RecordingObserver test double below lets you assert your observers honor it.
OpenTelemetry instrumentation
Derive a sealed diagnostics class from OrionInstrumentation. It exposes one ActivitySource and one Meter sharing a name and version. Create your instruments on Meter, and stamp every measurement through Tag(...) so the configured static tags are appended.
using System.Diagnostics.Metrics;
using Moongazing.Orion.Abstractions.Diagnostics;
public sealed class MyDiagnostics : OrionInstrumentation
{
public MyDiagnostics() : base("Moongazing.MyPackage", "1.0.0")
{
Things = Meter.CreateCounter<long>("my.things");
}
public Counter<long> Things { get; }
}
var diag = new MyDiagnostics();
// Set once at startup (single-threaded). These tags stamp every later measurement.
diag.SetStaticTags(new Dictionary<string, string> { ["tenant"] = tenantId });
// Tag(...) appends the static tags to the per-measurement tag.
diag.Things.Add(1, diag.Tag(new("outcome", "ok")));
When no static tags are configured, Tag(...) short-circuits to a single-element array, so the common single-tenant path stays allocation-light.
Instance-scoped instrumentation
When a single process holds several instances that share one Meter name (or tests run them in parallel), a name-filtered MeterListener cannot tell them apart and double-counts. Pass an instanceScopeId (and optionally extra instanceTags) to the scoped base constructor: the Meter is then created with the orion.instance tag (OrionInstrumentation.InstanceTagKey) and a non-null Meter.Scope, so a collector can split metrics per instance. The default name/version constructor leaves the Meter unscoped, matching prior behavior.
using System.Diagnostics.Metrics;
using Moongazing.Orion.Abstractions.Diagnostics;
public sealed class WorkerDiagnostics : OrionInstrumentation
{
public WorkerDiagnostics(string instanceScopeId)
: base("Moongazing.MyPackage", "1.0.0", instanceScopeId)
{
JobsProcessed = Meter.CreateCounter<long>("jobs.processed");
}
public Counter<long> JobsProcessed { get; }
}
using var first = new WorkerDiagnostics("worker-1");
using var second = new WorkerDiagnostics("worker-2");
// first.InstanceScopeId == "worker-1"; the Meter carries orion.instance=worker-1.
ListensTo filters a MeterListener to exactly one instance's instruments by Meter reference identity, so it is robust even if two instances are configured with the same instanceScopeId:
using var listener = new MeterListener();
listener.InstrumentPublished = (instrument, l) =>
{
if (OrionInstrumentation.ListensTo(instrument, first))
{
l.EnableMeasurementEvents(instrument); // only first's instruments are enabled
}
};
listener.Start();
The reserved orion.instance key cannot be supplied in instanceTags; doing so throws ArgumentException. Custom instanceTags are merged alongside it and stamp the Meter itself, independent of the per-measurement StaticTags.
Testable clock
Depend on IOrionClock instead of DateTime.UtcNow or Stopwatch. Production binds SystemOrionClock (over TimeProvider.System); tests bind FrozenOrionClock.
using Moongazing.Orion.Abstractions.Testing;
var clock = new FrozenOrionClock(); // starts frozen at 2026-01-01Z by default
long start = clock.GetTimestamp();
clock.Advance(TimeSpan.FromSeconds(31)); // drive a lease past expiry, no real delay
Assert.Equal(TimeSpan.FromSeconds(31), clock.GetElapsedTime(start));
Advance moves both the wall clock and the monotonic timestamp; SetUtcNow moves only the wall clock. Both reject going backward, matching a real monotonic clock.
Configuration
AddOrionAbstractions() uses TryAddSingleton, so the first registration wins. To supply your own clock (for example, a SystemOrionClock over a custom TimeProvider), register it before calling AddOrionAbstractions:
services.AddSingleton<IOrionClock>(new SystemOrionClock(myTimeProvider));
services.AddOrionAbstractions(); // TryAdd no-ops because a clock is already registered
Telemetry / Diagnostics
OrionInstrumentation is the integration point for OpenTelemetry. The ActivitySource and Meter are named with the value you pass to the base constructor, so wire them into your OpenTelemetry pipeline by that name:
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddSource("Moongazing.MyPackage"))
.WithMetrics(m => m.AddMeter("Moongazing.MyPackage"));
The static-tag pattern lets you split dashboards by tenant, region, or environment without standing up a second Meter. Tags configured after the host starts emitting do not retroactively apply to already-emitted measurements, which is why SetStaticTags is intended to be called once at startup.
Testing
- Reference
Orion.Abstractions.Testingfrom test projects and injectFrozenOrionClockwherever production injectsIOrionClock. Advancing the clock makes lease-expiry, grace-period, and scheduler tests deterministic and instant. SafeObserverInvokeris static and side-effect-free apart from the callbacks you pass, so it is straightforward to assert the no-op, happy, fault-swallowing, and cancellation-propagating paths directly.RecordingObserver<TObserver>(also inOrion.Abstractions.Testing) records every observer invocation and every swallowed fault at aSafeObserverInvokercall site. Pass itsTrack/TrackAsyncwrapper as the action and itsOnFaultas the fault hook, then assert your observers behave per the observer contract.
using Moongazing.Orion.Abstractions.Observers;
using Moongazing.Orion.Abstractions.Testing;
var recorder = new RecordingObserver<IMyObserver>(myObserver);
SafeObserverInvoker.Invoke(recorder.Observer, recorder.Track(o => o.OnSomething(payload)), recorder.OnFault);
Assert.True(recorder.WasInvoked); // the action ran to completion
Assert.False(recorder.Faulted); // no fault was swallowed
A micro-benchmark suite (BenchmarkDotNet) covers the allocation- and CPU-bearing surface: tag stamping, observer dispatch, and the clock seam. See benchmarks.md. No measured numbers are committed; run the suite locally to produce them for your hardware.
Packages
| Package | Purpose |
|---|---|
Orion.Abstractions |
The shared primitives above. |
Orion.Abstractions.Testing |
FrozenOrionClock and RecordingObserver test doubles. |
Versioning
Follows Semantic Versioning. The library multi-targets net8.0, net9.0, and net10.0. (The benchmark host runs on net8.0 and net9.0 only, because BenchmarkDotNet 0.14.0 has no .NET 10 job moniker.)
Documentation
- docs/FEATURES.md - a deeper breakdown of each feature and its public types and methods.
- docs/observer-contract.md - the normative contract for what an Orion observer may and may not do.
- docs/ROADMAP.md - ideas under consideration.
- benchmarks.md - what the benchmark suite measures and how to run it.
Contributing
Contributions are welcome. See CONTRIBUTING.md and the CODE_OF_CONDUCT.md.
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
-
net8.0
-
net9.0
NuGet packages (28)
Showing the top 5 NuGet packages that depend on Orion.Abstractions:
| Package | Downloads |
|---|---|
|
OrionAudit
EF Core change audit trail with JSON Patch diffs, multi-tenant support, time-travel reconstruction, and OpenTelemetry instrumentation. |
|
|
OrionPatch
Transactional outbox primitive for .NET. Enqueue messages inside an EF Core SaveChanges transaction; a background dispatcher hands them to a pluggable IOutboxSink at-least-once. Ships ChannelOutboxSink (in-process); broker sinks are opt-in sub-packages. |
|
|
OrionVault
Column-level transparent data encryption at rest for EF Core. AES-256-GCM with key rotation, [Encrypted] attribute, fluent API, bundled Roslyn analyzer, and OpenTelemetry instrumentation. |
|
|
OrionPatch.EntityFrameworkCore
EF Core storage backend for OrionPatch. Adds the OrionPatch_Outbox table; SaveChangesInterceptor flushes buffered messages into the user's transaction. |
|
|
OrionVault.EntityFrameworkCore
EF Core integration for OrionVault: value converters, [Encrypted] attribute model scanner, IsEncrypted() fluent API, IModelCustomizer wiring, and DbContext DI extensions. |
GitHub repositories
This package is not used by any popular GitHub repositories.