Getting Started with Pragmatic.Resilience
This guide covers adding resilience (retry, timeout, circuit breaker, bulkhead, fallback) to a Pragmatic.Design application.
Prerequisites
Section titled “Prerequisites”- .NET 10.0+
Pragmatic.Result(for error types)Pragmatic.SourceGeneratoranalyzer (for[ResiliencePolicy]integration with DomainActions)
Step 1: Add the Package
Section titled “Step 1: Add the Package”dotnet add package Pragmatic.ResilienceStep 2: Register Resilience Services
Section titled “Step 2: Register Resilience Services”Option A: Auto-Registration (SG)
Section titled “Option A: Auto-Registration (SG)”When Pragmatic.Resilience is referenced, the Source Generator auto-registers AddPragmaticResilience() with default settings. No manual registration needed for basic usage.
Option B: Explicit Registration
Section titled “Option B: Explicit Registration”For custom policies, register in IStartupStep:
[StartupStep]public class AppStartupStep : IStartupStep{ public void ConfigureServices( IServiceCollection services, IConfiguration configuration, IHostEnvironment environment) { services.AddPragmaticResilience(options => { options.Policies["external-api"] = new ResiliencePolicyOptions { Timeout = new() { Timeout = TimeSpan.FromSeconds(10) }, Retry = new() { MaxRetries = 3, BaseDelay = TimeSpan.FromMilliseconds(200) }, CircuitBreaker = new() { FailureThreshold = 5, BreakDuration = TimeSpan.FromSeconds(30) } }; }); }}You can also register individual named policies:
services.AddPragmaticResilience();
services.AddResiliencePolicy("database", o =>{ o.Timeout = new() { Timeout = TimeSpan.FromSeconds(5) }; o.Retry = new() { MaxRetries = 2, BackoffType = BackoffType.Constant, BaseDelay = TimeSpan.FromMilliseconds(100) };});Step 3: Add Resilience to a DomainAction
Section titled “Step 3: Add Resilience to a DomainAction”Annotate a DomainAction with [ResiliencePolicy] to wrap its execution with a named pipeline:
[DomainAction][ResiliencePolicy("external-api")]public partial class FetchUserAction : DomainAction<UserDto>{ public override async Task<Result<UserDto, IError>> Execute(CancellationToken ct) { // This execution is wrapped by the "external-api" resilience pipeline. // Exceptions trigger retry/circuit breaker. // Result failures pass through unchanged (they are business errors). var response = await httpClient.GetAsync("/users/123", ct); // ... }}The source generator injects IResiliencePipelineProvider and wraps ExecuteActionAsync with the specified pipeline.
Key design principle: only exceptions trigger resilience strategies. Result<T, E> failures are business errors (validation, not found) and pass through the pipeline unchanged. Retrying a “not found” or “validation failed” would be wrong.
Step 4: Use Pipelines Directly (Without DomainAction)
Section titled “Step 4: Use Pipelines Directly (Without DomainAction)”For code outside the action pattern, resolve IResiliencePipelineProvider and use a named pipeline:
public class ExternalApiClient(IResiliencePipelineProvider pipelines){ public async Task<string> FetchAsync(string url, CancellationToken ct) { var pipeline = pipelines.GetPipeline("external-api");
return await pipeline.ExecuteAsync( (ctx, token) => httpClient.GetStringAsync(url, token), new ResilienceContext { OperationName = "FetchData" }, ct); }}Step 5: Configure via appsettings.json
Section titled “Step 5: Configure via appsettings.json”Policies can be fully configured from JSON:
{ "Resilience": { "Default": { "Timeout": { "Timeout": "00:00:30", "TimeoutType": "Optimistic" } }, "Policies": { "external-api": { "Timeout": { "Timeout": "00:00:10" }, "Retry": { "MaxRetries": 3, "BaseDelay": "00:00:00.200", "BackoffType": "Exponential", "MaxDelay": "00:00:30", "UseJitter": true }, "CircuitBreaker": { "FailureThreshold": 5, "BreakDuration": "00:00:30" } } } }}Policy Resolution Order
Section titled “Policy Resolution Order”When GetPipeline(name) is called:
- Fluent overrides — policies registered via
AddResiliencePolicy(). - Configuration — policies from
ResilienceOptions.Policiesdictionary. - Default —
ResilienceOptions.Defaultif set. - Passthrough —
PassthroughPipeline.Instance(zero overhead, no wrapping).
Unknown policy names resolve to PassthroughPipeline — no runtime errors, no overhead.
Fluent Builder (No DI)
Section titled “Fluent Builder (No DI)”For standalone usage without DI:
var stateStore = new InMemoryCircuitBreakerStateStore();
var pipeline = new ResiliencePipelineBuilder() .AddRetry(o => { o.MaxRetries = 3; o.BackoffType = BackoffType.Exponential; }) .AddTimeout(o => o.Timeout = TimeSpan.FromSeconds(5)) .AddCircuitBreaker(stateStore, o => { o.FailureThreshold = 5; o.BreakDuration = TimeSpan.FromSeconds(30); }) .Build();
var result = await pipeline.ExecuteAsync( (ctx, ct) => httpClient.GetStringAsync(url, ct), new ResilienceContext { OperationName = "FetchData" });DI Registrations
Section titled “DI Registrations”AddPragmaticResilience() registers:
| Service | Lifetime | Description |
|---|---|---|
IResiliencePipelineProvider | Singleton | Resolves named pipelines |
ICircuitBreakerStateStore | Singleton | Default: InMemoryCircuitBreakerStateStore |
ResilienceOptions | Singleton | Configuration (via IOptions<ResilienceOptions>) |
Error Types
Section titled “Error Types”Resilience errors implement Pragmatic.Result.Error for integration with the Result pattern:
| Error | Code | HTTP Status | When |
|---|---|---|---|
TimeoutError | TIMEOUT | 504 | Operation exceeded timeout |
RetryExhaustedError | RETRY_EXHAUSTED | 503 | All retry attempts failed |
CircuitBrokenError | CIRCUIT_BROKEN | 503 | Circuit is open |
BulkheadRejectedError | BULKHEAD_REJECTED | 429 | Max concurrency exceeded |
Next Steps
Section titled “Next Steps”- Policies — detailed reference for each strategy