OrionResilience 0.6.0
dotnet add package OrionResilience --version 0.6.0
NuGet\Install-Package OrionResilience -Version 0.6.0
<PackageReference Include="OrionResilience" Version="0.6.0" />
<PackageVersion Include="OrionResilience" Version="0.6.0" />
<PackageReference Include="OrionResilience" />
paket add OrionResilience --version 0.6.0
#r "nuget: OrionResilience, 0.6.0"
#:package OrionResilience@0.6.0
#addin nuget:?package=OrionResilience&version=0.6.0
#tool nuget:?package=OrionResilience&version=0.6.0
<p align="center"> <img src="docs/logo.png" alt="OrionResilience" width="150" /> </p>
OrionResilience
One opinionated resilience vocabulary for the Orion family: retry with jitter presets and an overall timeout, executed over an OrionClock TimeProvider so every retry fast-forwards in tests, with OpenTelemetry by default and configured through options — not fluent chains.
Backoff is the most re-implemented 40 lines in any backend. Webhook delivery has its own exponential-backoff-with-jitter, lease acquisition has another, and every HttpClient caller writes a for loop with Task.Delay. They disagree on the base delay, the jitter algorithm, the cap, and what counts as retryable — and none of them are testable, because the delay is a real Task.Delay against the real clock, so the retry test either sleeps for real or doesn't exist.
OrionResilience is the opinionated preset layer. Retries and the timeout run on the family's OrionClock, so the same fake clock that fast-forwards leases and TTLs fast-forwards a whole retry sequence — a 4-retry pipeline with a 5-second cap completes in-test in microseconds. Every execution emits the family's orion.* OpenTelemetry signals.
Features
- Retry + timeout on
OrionClock— all delays and the overall timeout run on the clock'sTimeProvider. UnderFakeOrionClocka retry sequence fast-forwards deterministically; no real waits, no flaky "wait for it" tests. Backoffpresets —Exponential(configurable factor),DecorrelatedJitter(overflow-safe, injectable sampler for deterministic tests), andConstant, each with an optional cap.- Declared retryability —
RetryOn<TException>()andRetryOn(predicate), OR-composed; by default every exception is retryable until you narrow it. Retrying a non-idempotent operation is the caller's declared choice. - Typed failures —
TimeoutRejectedException(distinct from caller cancellation, which always propagates unchanged) andRetriesExhaustedException(carries the attempt count, wraps the last fault). - OpenTelemetry by default — a
Moongazing.OrionResiliencemeter/activity-source carryingorion.resilience.attempts,orion.resilience.retry.delay(ms), andorion.resilience.outcome(tagged with a frozen outcome), plus an execution span. Built on the family'sOrionInstrumentationspine, so multi-tenant / multi-region labels stamp every measurement. - AOT- and trim-clean, verified by a native-binary smoke test in CI. Multi-targets
net8.0,net9.0,net10.0.
Install
dotnet add package OrionResilience
Quick start
using Moongazing.OrionClock;
using Moongazing.OrionResilience;
var clock = new OrionClock(); // the family clock; in DI, resolve IOrionClock / OrionClock
var pipeline = new ResiliencePipeline(clock, new ResiliencePipelineOptions
{
MaxRetries = 4, // 1 initial attempt + 4 retries
Backoff = Backoff.DecorrelatedJitter(
baseDelay: TimeSpan.FromMilliseconds(200),
cap: TimeSpan.FromSeconds(5)),
Timeout = TimeSpan.FromSeconds(10), // overall budget for all attempts
}.RetryOn<HttpRequestException>() // only these are retryable
.RetryOn<TimeoutException>());
HttpResponseMessage response = await pipeline.ExecuteAsync(
async ct => await httpClient.SendAsync(request, ct),
cancellationToken);
ExecuteAsync returns the operation's result on success. It throws TimeoutRejectedException when the budget elapses, and RetriesExhaustedException (wrapping the last fault) when every retryable attempt fails. A caller-driven OperationCanceledException always propagates unchanged — it is never reclassified as a timeout.
Testing — retries fast-forward, no real waits
Point the pipeline at FakeOrionClock and advance time by hand. Because the backoff Task.Delay and the timeout CancellationTokenSource are both created through the clock's TimeProvider, advancing the clock fires them — the whole retry sequence runs instantly and deterministically.
using Moongazing.OrionClock.Testing;
var clock = new FakeOrionClock();
var pipeline = new ResiliencePipeline(clock, new ResiliencePipelineOptions
{
MaxRetries = 4,
Backoff = Backoff.Exponential(TimeSpan.FromMilliseconds(200), cap: TimeSpan.FromSeconds(5)),
});
var attempts = 0;
var execution = pipeline.ExecuteAsync(async ct =>
{
if (++attempts < 5) throw new InvalidOperationException("transient");
await Task.CompletedTask;
return "ok";
});
// Drive the scheduled backoff waits by advancing the clock, not by sleeping.
while (!execution.IsCompleted) clock.Advance(TimeSpan.FromMilliseconds(500));
Assert.Equal("ok", await execution); // recovered after 4 retries, in microseconds
Pair it with DeterministicFaultInjector (from Orion.Abstractions.Testing) to model a dependency that fails a fixed number of times, or recovers at a known instant, with no randomness.
Observability
Every execution records to a Moongazing.OrionResilience meter and activity source:
| Signal | Kind | Meaning |
|---|---|---|
orion.resilience.attempts |
counter | Individual attempts executed (including the first). |
orion.resilience.retry.delay |
histogram (ms) | The backoff waited before each retry, tagged with the 1-based attempt. |
orion.resilience.outcome |
counter | Completed executions, tagged outcome = success / failure / timeout / cancelled. |
Telemetry emits by default through a shared instance. Hand a ResilienceDiagnostics you own to the pipeline constructor when you want DI-managed lifetime or per-instance scoping.
Roadmap
Wave 1 (this release) ships retry + timeout on OrionClock, the jitter presets, and OpenTelemetry, AOT-clean. Circuit-breaker and hedging, OrionResult-typed errors, options-configured named pipelines (services.AddOrionResilience(...)), and typed-HttpClient integration land in later waves. See CHANGELOG.md.
OrionResilience curates and integrates proven strategies rather than inventing new ones; it is the family's opinionated preset layer, not a new resilience engine, and it is not a rate limiter or bulkhead (those compose from elsewhere in the family).
Versioning
Follows Semantic Versioning. Multi-targets net8.0, net9.0, and net10.0. Binds to Orion.Abstractions 1.x and OrionClock 0.9.x.
Documentation
- CHANGELOG.md — release notes.
Contributing
Contributions are welcome. See CONTRIBUTING.md and the CODE_OF_CONDUCT.md.
More from the Orion family
Focused .NET libraries built to one quality bar. Each is usable on its own; several share the small Orion.Abstractions contracts spine, but there is no deep dependency web — pick only what you need:
- Orion.Abstractions — the shared contracts spine: telemetry, options, result, clock
- OrionClock — a
TimeProvider-based clock with TTL / deadline vocabulary - OrionGuard — validation, guard clauses, DDD primitives, domain events
- OrionAudit — automatic EF Core change-audit trail
- OrionBeacon — leader election with fencing tokens
- OrionGrant — permission / authorization checks
- OrionKey — source-generated strongly-typed IDs
- OrionLedger — API-key issuance, verification, and rotation
- OrionLens — ambient correlation-context propagation
- OrionLock — distributed locks with fencing tokens
- OrionOnce — idempotency keys for exactly-once request handling
- OrionPatch — transactional outbox for EF Core
- OrionRelay — outbound webhook delivery (HMAC, retries, backoff)
- OrionResult — Result/Option types and a shared error vocabulary
- OrionSaga — sagas / process managers for long-running workflows
- OrionShade — sensitive-data redaction for logs and telemetry
- OrionStream — server-sent events / streaming hub
- OrionVault — field-level encryption for EF Core
See it all working together in OrionShowcase, a production-shaped banking sample.
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
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- Orion.Abstractions (>= 1.2.0)
- OrionClock (>= 0.9.0)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- Orion.Abstractions (>= 1.2.0)
- OrionClock (>= 0.9.0)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- Orion.Abstractions (>= 1.2.0)
- OrionClock (>= 0.9.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.6.0 | 105 | 8/9/2026 |