Source Generator
ZeroAlloc.Resilience uses a Roslyn IIncrementalGenerator to emit a proxy class for every annotated interface. This page shows exactly what is generated and how to inspect it.
What triggers generation
The generator activates on any interface decorated with at least one of [Retry], [Timeout], [RateLimit], or [CircuitBreaker]. It reads method signatures, collects effective policies (method-level shadows interface-level), validates fallback methods, and emits one file per annotated interface.
Example input
[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);
}
Generated output
// <auto-generated />
#nullable enable
using System.Threading;
using System.Threading.Tasks;
using ZeroAlloc.Resilience;
using Microsoft.Extensions.DependencyInjection;
namespace MyApp;
internal sealed class IExternalServiceResilienceProxy : global::MyApp.IExternalService
{
private readonly global::MyApp.IExternalService _inner;
private readonly global::ZeroAlloc.Resilience.RetryPolicy _retry;
private readonly global::ZeroAlloc.Resilience.TimeoutPolicy _timeout;
private readonly global::ZeroAlloc.Resilience.RateLimiter _rateLimiter;
private readonly global::ZeroAlloc.Resilience.CircuitBreakerPolicy _circuitBreaker;
public IExternalServiceResilienceProxy(
global::MyApp.IExternalService inner,
global::ZeroAlloc.Resilience.RetryPolicy retry,
global::ZeroAlloc.Resilience.TimeoutPolicy timeout,
global::ZeroAlloc.Resilience.RateLimiter rateLimiter,
global::ZeroAlloc.Resilience.CircuitBreakerPolicy circuitBreaker)
{
_inner = inner;
_retry = retry;
_timeout = timeout;
_rateLimiter = rateLimiter;
_circuitBreaker = circuitBreaker;
}
public async global::System.Threading.Tasks.ValueTask<string> FetchAsync(
string id, global::System.Threading.CancellationToken ct)
{
// 1. Rate limit check
if (!_rateLimiter.TryAcquire())
throw new global::ZeroAlloc.Resilience.ResilienceException(
global::ZeroAlloc.Resilience.ResiliencePolicy.RateLimit, "Rate limit exceeded.");
// 2. Circuit breaker check — calls fallback if open
if (!_circuitBreaker.CanExecute())
return await _inner.FetchFallback(id, ct).ConfigureAwait(false);
// 3. Total timeout CTS — wraps all retries and backoff
using var __totalCts = global::System.Threading.CancellationTokenSource.CreateLinkedTokenSource(ct);
__totalCts.CancelAfter(5000);
// 4. Retry loop
global::System.Exception? __lastEx = null;
for (int __attempt = 0; __attempt < 3; __attempt++)
{
// Per-attempt timeout CTS
using var __attemptCts = global::System.Threading.CancellationTokenSource
.CreateLinkedTokenSource(__totalCts.Token);
__attemptCts.CancelAfter(1000);
var __ct = __attemptCts.Token;
try
{
var __result = await _inner.FetchAsync(id, __ct).ConfigureAwait(false);
_circuitBreaker.OnSuccess();
return __result;
}
catch (global::System.Exception __ex)
{
__lastEx = __ex;
_circuitBreaker.OnFailure(__ex);
if (__totalCts.IsCancellationRequested) break;
if (__attempt == 2) break;
// Jitter backoff: base * 2^attempt + random(0, base * 2^attempt / 2)
await global::System.Threading.Tasks.Task.Delay(
200 * (1 << __attempt) + global::System.Random.Shared.Next(
0, global::System.Math.Max(1, 200 * (1 << __attempt) / 2)),
__totalCts.Token).ConfigureAwait(false);
}
}
throw new global::ZeroAlloc.Resilience.ResilienceException(
global::ZeroAlloc.Resilience.ResiliencePolicy.Retry,
"All retry attempts failed.", __lastEx);
}
// FetchFallback — no policy attributes → passthrough
public async global::System.Threading.Tasks.ValueTask<string> FetchFallback(
string id, global::System.Threading.CancellationToken ct)
=> await _inner.FetchFallback(id, ct).ConfigureAwait(false);
}
// DI extension — one line registers everything
public static partial class ResilienceServiceCollectionExtensions
{
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection
AddExternalServiceResilience<TImpl>(
this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services)
where TImpl : class, global::MyApp.IExternalService
{
services.AddTransient<TImpl>();
services.AddSingleton(new global::ZeroAlloc.Resilience.RetryPolicy(3, 200, true, 1000));
services.AddSingleton(new global::ZeroAlloc.Resilience.TimeoutPolicy(5000));
services.AddSingleton(new global::ZeroAlloc.Resilience.RateLimiter(100, 10,
global::ZeroAlloc.Resilience.RateLimitScope.Shared));
services.AddSingleton(new global::ZeroAlloc.Resilience.CircuitBreakerPolicy(5, 1000, 1));
services.AddTransient<global::MyApp.IExternalService>(sp =>
new IExternalServiceResilienceProxy(
sp.GetRequiredService<TImpl>(),
sp.GetRequiredService<global::ZeroAlloc.Resilience.RetryPolicy>(),
sp.GetRequiredService<global::ZeroAlloc.Resilience.TimeoutPolicy>(),
sp.GetRequiredService<global::ZeroAlloc.Resilience.RateLimiter>(),
sp.GetRequiredService<global::ZeroAlloc.Resilience.CircuitBreakerPolicy>()));
return services;
}
}
Key generation decisions
Policy values baked as literals
Retry config (MaxAttempts, BackoffMs, PerAttemptTimeoutMs) is baked as integer literals in the generated loop body. This means method-level overrides work correctly — each method's loop uses its own values, not a shared policy object field.
Passthrough methods
Any interface method that has no policy attributes (including fallback methods) is emitted as a direct delegation with no try/catch overhead:
public async ValueTask<string> FetchFallback(string id, CancellationToken ct)
=> await _inner.FetchFallback(id, ct).ConfigureAwait(false);
No try/catch when not needed
If a method has no circuit breaker and does not return Result<T>, the generator emits a direct return instead of a wrapped try/catch — eliminating the exception-handler overhead entirely.
Rate limit + circuit breaker are checks, not wrappers
Both are simple if (!...) checks before the inner call, not try/catch wrappers. A rejected call exits immediately with a throw (or fallback) — the inner implementation is never invoked.
Inspecting the generated output
Visual Studio / Rider: Expand the project's Analyzers → ZeroAlloc.Resilience.Generator node in Solution Explorer.
Command line:
dotnet build
# Generated files are under:
# obj/Debug/{tfm}/generated/ZeroAlloc.Resilience.Generator/
# ZeroAlloc.Resilience.Generator.ResilienceGenerator/
# {Namespace}_{InterfaceName}.Resilience.g.cs
Go to Definition: Place your cursor on the generated proxy constructor or any method and press F12.
Diagnostics
| ID | Severity | Trigger |
|---|---|---|
| ZR0001 | Error | Fallback method not found or its signature does not match |
| ZR0002 | Warning | [Timeout] or PerAttemptTimeoutMs configured but method has no CancellationToken parameter |