Polly Resilience Pipeline in .NET
The Resilience Pipeline, introduced in .NET 8, is a standardized API for building and composing resilience policies with first-class support for dependency injection, configuration, and telemetry. Instead of manually wrapping policies and managing instances, you register resilience pipelines in the service container with AddResiliencePipeline, configure them declaratively, and inject them where needed. This modern approach reduces boilerplate, improves testability, and integrates cleanly with .NET Minimal APIs and structured logging.
What Changed: Classic Polly vs. Resilience Pipeline
Classic Polly requires manual policy creation and composition:
// Old way: manual policy creation and wrapping
var retryPolicy = Policy.Handle<Exception>().WaitAndRetryAsync(...);
var timeoutPolicy = Policy.TimeoutAsync(...);
var combined = Policy.WrapAsync(timeoutPolicy, retryPolicy);
var result = await combined.ExecuteAsync(() => MyOperation());
Resilience Pipeline uses dependency injection and declarative configuration:
// New way: register pipeline in service container
services.AddResiliencePipeline("my-service-pipeline", builder =>
{
builder
.AddRetry(new RetryStrategyOptions(...))
.AddTimeout(TimeSpan.FromSeconds(5));
});
// Inject and use
var pipeline = resiliencePipelineProvider.GetPipeline("my-service-pipeline");
var result = await pipeline.ExecuteAsync(() => MyOperation());
Setting Up Resilience Pipeline
Install the required NuGet package:
dotnet add package Microsoft.Extensions.Resilience
In your Program.cs or startup code:
using Microsoft.Extensions.Resilience;
using System.Net.Http;
var builder = WebApplication.CreateBuilder(args);
// Register resilience pipeline with retry and timeout
builder.Services.AddResiliencePipeline("external-api", pipeline =>
{
pipeline
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.HandleResult<HttpResponseMessage>(r =>
!r.IsSuccessStatusCode
),
Delay = TimeSpan.FromMilliseconds(100),
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true
})
.AddTimeout(TimeSpan.FromSeconds(5));
});
var app = builder.Build();
app.MapGet("/data", FetchData).WithName("GetData");
app.Run();
async Task<IResult> FetchData(
ResiliencePipelineProvider<HttpClient> pipelineProvider,
HttpClient httpClient)
{
var pipeline = pipelineProvider.GetPipeline("external-api");
try
{
var result = await pipeline.ExecuteAsync(
async () => await httpClient.GetStringAsync("https://api.example.com/data")
);
return Results.Ok(result);
}
catch (Exception ex)
{
return Results.StatusCode(500);
}
}
Resilience Pipeline with Circuit Breaker
Add circuit breaker to the pipeline:
using Microsoft.Extensions.Resilience;
using System.Net;
builder.Services.AddResiliencePipeline("payment-service", pipeline =>
{
pipeline
.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.HandleResult<HttpResponseMessage>(r =>
r.StatusCode == HttpStatusCode.ServiceUnavailable ||
r.StatusCode == HttpStatusCode.TooManyRequests
),
MaxRetryAttempts = 2,
Delay = TimeSpan.FromMilliseconds(200),
BackoffType = DelayBackoffType.Exponential,
UseJitter = true
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
ShouldHandle = new PredicateBuilder()
.Handle<HttpRequestException>()
.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode),
FailureRatio = 0.5, // Open circuit at 50% failure rate
MinimumThroughput = 5, // Require 5 requests to evaluate
SamplingDuration = TimeSpan.FromSeconds(10),
BreakDuration = TimeSpan.FromSeconds(30),
OnCircuitOpenAsync = async (args) =>
{
Console.WriteLine("Circuit opened: payment service degraded");
await Task.CompletedTask;
}
})
.AddTimeout(TimeSpan.FromSeconds(10));
});
Configuring from appsettings.json
Load resilience pipeline configuration from JSON:
appsettings.json:
{
"Resilience": {
"Pipelines": {
"external-api": {
"Retry": {
"MaxRetryAttempts": 3,
"Delay": "00:00:00.100",
"BackoffType": "Exponential",
"UseJitter": true
},
"CircuitBreaker": {
"FailureRatio": 0.5,
"MinimumThroughput": 5,
"SamplingDuration": "00:00:10",
"BreakDuration": "00:00:30"
},
"Timeout": {
"Duration": "00:00:05"
}
}
}
}
}
Program.cs:
using Microsoft.Extensions.Resilience;
var builder = WebApplication.CreateBuilder(args);
// Load configuration from appsettings
var resilienceConfig = builder.Configuration.GetSection("Resilience:Pipelines:external-api");
builder.Services.AddResiliencePipeline("external-api", pipeline =>
{
var retryOptions = resilienceConfig.GetSection("Retry").Get<RetryStrategyOptions>();
var cbOptions = resilienceConfig.GetSection("CircuitBreaker").Get<CircuitBreakerStrategyOptions>();
var timeoutDuration = resilienceConfig.GetValue<TimeSpan>("Timeout:Duration");
pipeline
.AddRetry(retryOptions)
.AddCircuitBreaker(cbOptions)
.AddTimeout(timeoutDuration);
});
Resilience Pipeline with Telemetry
Resilience Pipeline integrates with .NET structured logging. Failures are logged automatically:
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Resilience;
var builder = WebApplication.CreateBuilder(args);
// Enable structured logging
builder.Logging.AddConsole();
builder.Logging.SetMinimumLevel(LogLevel.Information);
builder.Services.AddResiliencePipeline("api-call", pipeline =>
{
pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(100),
BackoffType = DelayBackoffType.Exponential
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.5,
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(30)
});
});
var app = builder.Build();
app.MapGet("/call", async (ResiliencePipelineProvider<HttpClient> provider, HttpClient client, ILogger<Program> logger) =>
{
var pipeline = provider.GetPipeline("api-call");
try
{
var result = await pipeline.ExecuteAsync(
async () => await client.GetStringAsync("https://api.example.com/data")
);
logger.LogInformation("API call succeeded");
return Results.Ok(result);
}
catch (Exception ex)
{
logger.LogError(ex, "API call failed after resilience policies exhausted");
return Results.StatusCode(500);
}
});
app.Run();
Log output:
info: Microsoft.Extensions.Resilience.Pipeline
Retry strategy executed. Attempt: 1, BackoffDuration: 100ms
warn: Microsoft.Extensions.Resilience.Pipeline
Circuit breaker state changed to Open. FailureRatio: 0.6
error: System.Net.Http.HttpRequestException: Connection refused
Using Resilience Pipeline in a Service Class
Inject the pipeline provider into a service and use it:
using Microsoft.Extensions.Resilience;
using System.Net.Http;
public class ExternalDataService
{
private readonly HttpClient _httpClient;
private readonly ResiliencePipelineProvider<HttpClient> _pipelineProvider;
public ExternalDataService(
HttpClient httpClient,
ResiliencePipelineProvider<HttpClient> pipelineProvider)
{
_httpClient = httpClient;
_pipelineProvider = pipelineProvider;
}
public async Task<string> FetchDataAsync(string endpoint)
{
var pipeline = _pipelineProvider.GetPipeline("external-api");
return await pipeline.ExecuteAsync(
async () => await _httpClient.GetStringAsync(endpoint)
);
}
}
// Register in Program.cs
builder.Services.AddHttpClient<ExternalDataService>();
builder.Services.AddResiliencePipeline("external-api", pipeline =>
{
pipeline
.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromMilliseconds(100),
BackoffType = DelayBackoffType.Exponential
})
.AddTimeout(TimeSpan.FromSeconds(5));
});
Comparing Classic Polly and Resilience Pipeline
| Aspect | Classic Polly | Resilience Pipeline |
|---|---|---|
| Setup | Manual policy creation | Declarative service registration |
| Dependency Injection | Manual wrapping | Built-in DI support |
| Configuration | Hardcoded or custom | appsettings.json friendly |
| Telemetry | Manual logging | Integrated structured logging |
| Testing | Mock policy instances | Mock pipeline provider |
| Composability | Policy.WrapAsync(...) | Builder fluent API |
Testing Resilience Pipelines
Unit test a service using the pipeline:
using Microsoft.Extensions.Resilience;
using Xunit;
using System;
using System.Threading.Tasks;
using Moq;
public class ResiliencePipelineTests
{
[Fact]
public async Task FetchDataRetriesOnFailure()
{
// Mock pipeline provider
var pipelineProviderMock = new Mock<ResiliencePipelineProvider<HttpClient>>();
var pipelineMock = new Mock<ResiliencePipeline>();
pipelineProviderMock
.Setup(p => p.GetPipeline("external-api"))
.Returns(pipelineMock.Object);
pipelineMock
.Setup(p => p.ExecuteAsync<string>(It.IsAny<Func<Task<string>>>()))
.ReturnsAsync("Success");
var service = new ExternalDataService(
new HttpClient(),
pipelineProviderMock.Object
);
var result = await service.FetchDataAsync("https://api.example.com/data");
Assert.Equal("Success", result);
pipelineMock.Verify(
p => p.ExecuteAsync<string>(It.IsAny<Func<Task<string>>>()),
Times.Once
);
}
}
Key Takeaways
- Resilience Pipeline (.NET 8+) replaces manual policy wrapping with declarative, DI-friendly configuration.
- Register pipelines in
AddResiliencePipelinewith a name and builder. - Inject via
ResiliencePipelineProvider<T>and callGetPipeline(name). - Configuration can be loaded from appsettings.json for environment-specific settings.
- Telemetry is built-in; failures are logged via structured logging automatically.
Frequently Asked Questions
Can I migrate existing Classic Polly code to Resilience Pipeline?
Yes. Resilience Pipeline uses similar strategy options; you refactor manual policy wrapping into declarative builder calls. The transition is straightforward for simple policies.
Does Resilience Pipeline support all Polly policies?
Not yet. As of .NET 8, Resilience Pipeline supports Retry, CircuitBreaker, Timeout, and RateLimit. Custom policies can still use Classic Polly. Expect more strategies in future releases.
How do I test with Resilience Pipeline?
Mock ResiliencePipelineProvider<T> and ResiliencePipeline in unit tests. Dependency injection makes testing simpler than classic Polly's manual wrapping.
What if I need both classic Polly and Resilience Pipeline?
You can use both simultaneously. Resilience Pipeline is new; classic Polly remains supported. Gradually migrate new code to Resilience Pipeline.