Source-generated, zero-allocation resilience policies for .NET. Add [Retry], [Timeout], [RateLimit], and [CircuitBreaker] to an interface; the generator emits a proxy composing all policies in declaration order. AOT-safe.
Install
| Product | Versions |
|---|---|
| .NET |
README
ZeroAlloc.Resilience
Source-generated, zero-allocation resilience policies for .NET.
Add [Retry], [Timeout], [RateLimit], and [CircuitBreaker] to an interface. A Roslyn source generator emits a proxy class that composes all policies in declaration order with no heap allocation on the happy path (beyond the unavoidable CancellationTokenSource for timeout). AOT-safe.
Quick start
dotnet add package ZeroAlloc.Resilience
[Retry(MaxAttempts = 3, BackoffMs = 200, Jitter = true, PerAttemptTimeoutMs = 1_000)]
[Timeout(Ms = 5_000)]
[RateLimit(MaxPerSecond = 100, BurstSize = 10)]
[CircuitBreaker(MaxFailures = 5, ResetMs = 1_000, HalfOpenProbes = 1, Fallback = nameof(FetchFallback))]
public interface IExternalService
{
ValueTask<string> FetchAsync(string id, CancellationToken ct);
ValueTask<string> FetchFallback(string id, CancellationToken ct);
}
// Register β one line wires everything
builder.Services.AddExternalServiceResilience<ExternalServiceImpl>();
Inject IExternalService anywhere β all policies are transparent to the caller.
Features
| Feature | Notes |
|---|---|
| Zero allocation on happy path | Policy checks are integer comparisons and CAS operations |
| AOT / trimmer safe | Generated proxy is concrete; no reflection at runtime |
| Retry with exponential backoff | Jitter, per-attempt timeout, total timeout all configurable |
| Timeout | Total operation timeout wrapping all retries and backoff delays |
| Rate limiting | Lock-free token bucket; Shared (singleton) or Instance (per-proxy) scope |
| Circuit breaker | Closed β Open β HalfOpen FSM backed by ZeroAlloc.StateMachine (concurrent CAS) |
| Fallback | Method called when circuit is open β same signature, no allocation |
Result<T> support |
Return Result<T> to get failures without exceptions |
| Method-level overrides | Any attribute on a method shadows the interface-level config for that method |
| DI integration | Generated Add{Service}Resilience<TImpl>() extension registers everything |
Policy execution order
Policies execute in this order on every call:
RateLimit β CircuitBreaker β Timeout β Retry (with PerAttemptTimeout inside)
Each policy runs before the inner call is even attempted. If the rate limiter rejects, the circuit breaker and retry logic are never reached.
Attribute overview
[Retry]
[Retry(MaxAttempts = 3, BackoffMs = 200, Jitter = true, PerAttemptTimeoutMs = 1_000)]
| Property | Type | Default | Description |
|---|---|---|---|
MaxAttempts |
int |
3 |
Total attempts (initial + retries) |
BackoffMs |
int |
200 |
Base backoff ms; actual = BackoffMs * 2^attempt |
Jitter |
bool |
false |
Add up to 50% random jitter to prevent thundering-herd |
PerAttemptTimeoutMs |
int |
0 |
Per-attempt cancellation timeout; 0 = disabled |
[Timeout]
[Timeout(Ms = 5_000)]
| Property | Type | Default | Description |
|---|---|---|---|
Ms |
int |
required | Total operation timeout wrapping all retries and backoff |
[RateLimit]
[RateLimit(MaxPerSecond = 100, BurstSize = 10, Scope = RateLimitScope.Shared)]
| Property | Type | Default | Description |
|---|---|---|---|
MaxPerSecond |
int |
required | Token refill rate |
BurstSize |
int |
1 |
Initial and peak token count |
Scope |
RateLimitScope |
Shared |
Shared = singleton per interface; Instance = per proxy |
[CircuitBreaker]
[CircuitBreaker(MaxFailures = 5, ResetMs = 1_000, HalfOpenProbes = 1, Fallback = nameof(FetchFallback))]
| Property | Type | Default | Description |
|---|---|---|---|
MaxFailures |
int |
5 |
Consecutive failures that trip Closed β Open |
ResetMs |
int |
1_000 |
Delay before Open β HalfOpen |
HalfOpenProbes |
int |
1 |
Successes required to close from HalfOpen |
Fallback |
string? |
null |
Fallback method name β called when circuit is Open |
Method-level overrides
Apply attributes directly to methods to override the interface-level config for that method only:
[Retry(MaxAttempts = 3, BackoffMs = 200)]
public interface IExternalService
{
ValueTask<string> FetchAsync(string id, CancellationToken ct); // uses interface-level
[Retry(MaxAttempts = 1)] // POST is not idempotent β one attempt only
ValueTask PostAsync(string data, CancellationToken ct);
}
Method-level attributes shadow interface-level ones entirely for that method β they are not additive.
Failure surface
| Return type | On failure |
|---|---|
ValueTask<T> / Task<T> |
ResilienceException thrown with Policy property |
ValueTask<Result<T>> / Task<Result<T>> |
Result.Failure(...) returned β no throw |
try
{
var result = await service.FetchAsync("id", ct);
}
catch (ResilienceException ex) when (ex.Policy == ResiliencePolicy.CircuitBreaker)
{
// circuit was open and no fallback was configured
}
Diagnostics
| ID | Severity | Description |
|---|---|---|
| ZR0001 | Error | Fallback method not found or signature mismatch |
| ZR0002 | Warning | Timeout configured but method has no CancellationToken |
Documentation
Full docs live in docs/:
- Getting Started
- Attribute Reference
- Source Generator
- Performance
- Core concepts: Retry Β· Timeout Β· Rate Limit Β· Circuit Breaker Β· Execution Order
- Guides: Fallback Β· Method-Level Overrides Β· Result Return Types Β· DI Registration
License
MIT
Changes from Previous Version
Adoption Assessment
Assess this package against its supported frameworks, license, maintenance signals and dependency graph before making it a project standard.
Related Guides & Workflows
Learn how to evaluate, integrate, or audit this package in your .NET project.