Skip to content

Getting Started with Pragmatic.Resilience

This guide covers adding resilience (retry, timeout, circuit breaker, bulkhead, fallback) to a Pragmatic.Design application.

  • .NET 10.0+
  • Pragmatic.Result (for error types)
  • Pragmatic.SourceGenerator analyzer (for [ResiliencePolicy] integration with DomainActions)
Terminal window
dotnet add package Pragmatic.Resilience

When Pragmatic.Resilience is referenced, the Source Generator auto-registers AddPragmaticResilience() with default settings. No manual registration needed for basic usage.

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) };
});

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);
}
}

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"
}
}
}
}
}

When GetPipeline(name) is called:

  1. Fluent overrides — policies registered via AddResiliencePolicy().
  2. Configuration — policies from ResilienceOptions.Policies dictionary.
  3. DefaultResilienceOptions.Default if set.
  4. PassthroughPassthroughPipeline.Instance (zero overhead, no wrapping).

Unknown policy names resolve to PassthroughPipeline — no runtime errors, no overhead.

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" });

AddPragmaticResilience() registers:

ServiceLifetimeDescription
IResiliencePipelineProviderSingletonResolves named pipelines
ICircuitBreakerStateStoreSingletonDefault: InMemoryCircuitBreakerStateStore
ResilienceOptionsSingletonConfiguration (via IOptions<ResilienceOptions>)

Resilience errors implement Pragmatic.Result.Error for integration with the Result pattern:

ErrorCodeHTTP StatusWhen
TimeoutErrorTIMEOUT504Operation exceeded timeout
RetryExhaustedErrorRETRY_EXHAUSTED503All retry attempts failed
CircuitBrokenErrorCIRCUIT_BROKEN503Circuit is open
BulkheadRejectedErrorBULKHEAD_REJECTED429Max concurrency exceeded
  • Policies — detailed reference for each strategy