NetEvolve.Pulse.Polly provides Polly v8 resilience policies for the Pulse CQRS mediator through interceptor integration. Add retry, circuit breaker, timeout, bulkhead, and fallback strategies to command handlers, query handlers, and event handlers with fluent API configuration.
- Polly v8 Integration: Seamless integration with Polly's modern resilience pipeline API
- Per-Handler Policies: Fine-grained control over resilience strategies for specific handlers
- Multiple Policy Types: Retry, circuit breaker, timeout, bulkhead, and fallback strategies
- Fluent API: Type-safe configuration through extension methods on
IMediatorBuilder - LIFO-Aware: Works with Pulse's interceptor execution order for predictable behavior
- Thread-Safe: Polly pipelines are singleton-safe and designed for concurrent use
Install-Package NetEvolve.Pulse.Pollydotnet add package NetEvolve.Pulse.Polly<PackageReference Include="NetEvolve.Pulse.Polly" Version="x.x.x" />using Microsoft.Extensions.DependencyInjection;
using NetEvolve.Pulse;
using NetEvolve.Pulse.Polly;
using Polly;
var services = new ServiceCollection();
services.AddPulse(config => config
.AddCommandHandler<CreateOrderCommand, OrderResult, CreateOrderHandler>()
.AddPollyRequestPolicies<CreateOrderCommand, OrderResult>(pipeline => pipeline
.AddRetry(new RetryStrategyOptions<OrderResult>
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential
})));
using var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();
// Handler execution is protected by retry policy
var result = await mediator.SendAsync<CreateOrderCommand, OrderResult>(
new CreateOrderCommand("SKU-123"));Apply retry logic to a specific command or query handler:
services.AddPulse(config => config
.AddCommandHandler<CreateOrderCommand, OrderResult, CreateOrderHandler>()
.AddPollyRequestPolicies<CreateOrderCommand, OrderResult>(pipeline => pipeline
.AddRetry(new RetryStrategyOptions<OrderResult>
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(2),
BackoffType = DelayBackoffType.Exponential,
OnRetry = args =>
{
Console.WriteLine($"Retry attempt {args.AttemptNumber}");
return default;
}
})));Protect external service calls with a circuit breaker:
services.AddPulse(config => config
.AddQueryHandler<GetUserQuery, User, GetUserQueryHandler>()
.AddPollyRequestPolicies<GetUserQuery, User>(pipeline => pipeline
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<User>
{
FailureRatio = 0.5, // Break after 50% failures
MinimumThroughput = 10, // Minimum 10 requests in window
BreakDuration = TimeSpan.FromSeconds(30),
SamplingDuration = TimeSpan.FromMinutes(1),
OnOpened = args =>
{
Console.WriteLine("Circuit breaker opened!");
return default;
}
})));Layer multiple resilience strategies:
services.AddPulse(config => config
.AddQueryHandler<SearchProductsQuery, ProductList, SearchProductsHandler>()
.AddPollyRequestPolicies<SearchProductsQuery, ProductList>(pipeline => pipeline
.AddTimeout(TimeSpan.FromSeconds(30)) // Outermost: Total timeout
.AddRetry(new RetryStrategyOptions<ProductList>
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(2),
BackoffType = DelayBackoffType.Exponential
}) // Middle: Retry transient failures
.AddCircuitBreaker(new CircuitBreakerStrategyOptions<ProductList>
{
FailureRatio = 0.7,
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(15)
}))); // Innermost: Circuit breakerFor commands that don't return a response:
services.AddPulse(config => config
.AddCommandHandler<DeleteOrderCommand, DeleteOrderHandler>()
.AddPollyRequestPolicies<DeleteOrderCommand>(pipeline => pipeline
.AddRetry(new RetryStrategyOptions<Void>
{
MaxRetryAttempts = 2,
Delay = TimeSpan.FromSeconds(1)
})
.AddTimeout(TimeSpan.FromSeconds(10))));Apply policies to event processing:
services.AddPulse(config => config
.AddEventHandler<OrderCreatedEvent, SendEmailHandler>()
.AddEventHandler<OrderCreatedEvent, UpdateInventoryHandler>()
.AddPollyEventPolicies<OrderCreatedEvent>(pipeline => pipeline
.AddTimeout(TimeSpan.FromSeconds(10))
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
FailureRatio = 0.7,
MinimumThroughput = 5,
BreakDuration = TimeSpan.FromSeconds(15)
})));IEventOutbox for reliable event delivery instead of aggressive retries.
Limit concurrent executions to prevent resource exhaustion:
services.AddPulse(config => config
.AddCommandHandler<ImportDataCommand, ImportResult, ImportDataHandler>()
.AddPollyRequestPolicies<ImportDataCommand, ImportResult>(pipeline => pipeline
.AddConcurrencyLimiter(new ConcurrencyLimiterOptions
{
PermitLimit = 5, // Max 5 concurrent executions
QueueLimit = 10 // Queue up to 10 waiting requests
})));Provide alternative responses on failure:
services.AddPulse(config => config
.AddQueryHandler<GetCachedDataQuery, DataResult, GetCachedDataHandler>()
.AddPollyRequestPolicies<GetCachedDataQuery, DataResult>(pipeline => pipeline
.AddFallback(new FallbackStrategyOptions<DataResult>
{
FallbackAction = args => Outcome.FromResultAsValueTask(
new DataResult { IsFromCache = true, Data = "Default" })
})));Pulse interceptors execute in LIFO (Last-In, First-Out) order. The last registered interceptor runs first. Plan your policy chain accordingly:
config
.AddCommandHandler<CreateOrder, Result, CreateOrderHandler>()
.AddValidationInterceptor<CreateOrder, Result>() // Executes third (innermost)
.AddPollyRequestPolicies<CreateOrder, Result>(...) // Executes second
.AddActivityAndMetrics(); // Executes first (outermost)Within a single Polly pipeline, strategies execute in the order they are added:
pipeline
.AddTimeout(...) // Outermost strategy
.AddRetry(...) // Middle strategy
.AddCircuitBreaker(...) // Innermost strategy- Use exponential backoff for transient failures (network, database connections)
- Keep
MaxRetryAttemptsconservative (2-3 for most scenarios) - Add jitter to prevent thundering herd:
UseJitter = true - Log retry attempts for observability
- Apply to external dependencies (APIs, databases, message queues)
- Set realistic
FailureRatio(0.5-0.7) andMinimumThroughputvalues - Monitor circuit breaker state transitions for alerts
- Use separate circuit breakers per dependency
- Set based on P99 latency + retry overhead
- Use shorter timeouts for events than requests
- Consider async operations - timeout should exceed sum of all downstream calls
- Combine with cancellation tokens for proper cleanup
- Use for resource-intensive operations (file processing, heavy computations)
- Set
PermitLimitbased on available resources (CPU cores, memory) - Monitor queue saturation for capacity planning
- Register pipelines with Singleton lifetime (default) for optimal performance
- Polly pipelines are thread-safe and stateless (except circuit breaker state)
- Reuse pipelines across requests - avoid creating per-request instances
- Profile policy overhead in production scenarios
- Be conservative with retry policies on events (multiple handlers amplify effects)
- Use shorter timeouts than requests to keep event processing responsive
- Consider
IEventOutboxpattern for guaranteed delivery vs. aggressive retries - Monitor event handler failures separately from request failures
For different policies per handler type, use keyed services:
services.AddKeyedSingleton("critical", sp =>
{
var builder = new ResiliencePipelineBuilder<OrderResult>();
builder.AddRetry(new RetryStrategyOptions<OrderResult> { MaxRetryAttempts = 5 });
return builder.Build();
});
services.AddKeyedSingleton("standard", sp =>
{
var builder = new ResiliencePipelineBuilder<OrderResult>();
builder.AddRetry(new RetryStrategyOptions<OrderResult> { MaxRetryAttempts = 2 });
return builder.Build();
});Polly v8 provides built-in telemetry through System.Diagnostics:
// Polly emits metrics to these meter names:
// - Polly.Retry
// - Polly.CircuitBreaker
// - Polly.Timeout
// - Polly.RateLimiter
// Example: Monitor circuit breaker state
var meterListener = new MeterListener();
meterListener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name == "Polly.CircuitBreaker")
{
listener.EnableMeasurementEvents(instrument, null);
}
};For integration with Pulse's AddActivityAndMetrics(), policy overhead is included in handler execution time.
| Approach | Pros | Cons |
|---|---|---|
| Polly Interceptors | Declarative, reusable, testable, composable with other interceptors | LIFO ordering requires planning |
| Manual Polly in Handlers | Fine-grained control, explicit | Repetitive code, hard to test, scattered logic |
| Middleware/Filters | Request-level scope | Not handler-specific, can't differentiate commands/queries |
- .NET 8.0, .NET 9.0, or .NET 10.0
- Polly v8.0 or later
Microsoft.Extensions.DependencyInjectionfor service registration
Contributions are welcome! Please read the Contributing Guidelines before submitting a pull request.
- Issues: Report bugs or request features on GitHub Issues
- Documentation: Read the full documentation at https://github.com/dailydevops/pulse
This project is licensed under the MIT License - see the LICENSE file for details.
- NetEvolve.Pulse - Core CQRS mediator
- NetEvolve.Pulse.Dapr - Dapr pub/sub integration for event dispatch
- NetEvolve.Pulse.Extensibility - Extensibility contracts
- NetEvolve.Pulse.EntityFramework - Entity Framework Core outbox persistence
- NetEvolve.Pulse.SqlServer - SQL Server ADO.NET outbox persistence
Note
Made with ❤️ by the NetEvolve Team