NetEvolve.Pulse.Polly
0.10.11
Prefix Reserved
dotnet add package NetEvolve.Pulse.Polly --version 0.10.11
NuGet\Install-Package NetEvolve.Pulse.Polly -Version 0.10.11
<PackageReference Include="NetEvolve.Pulse.Polly" Version="0.10.11" />
<PackageVersion Include="NetEvolve.Pulse.Polly" Version="0.10.11" />
<PackageReference Include="NetEvolve.Pulse.Polly" />
paket add NetEvolve.Pulse.Polly --version 0.10.11
#r "nuget: NetEvolve.Pulse.Polly, 0.10.11"
#:package NetEvolve.Pulse.Polly@0.10.11
#addin nuget:?package=NetEvolve.Pulse.Polly&version=0.10.11
#tool nuget:?package=NetEvolve.Pulse.Polly&version=0.10.11
NetEvolve.Pulse.Polly
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.
Features
- 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
IMediatorConfigurator - LIFO-Aware: Works with Pulse's interceptor execution order for predictable behavior
- Thread-Safe: Polly pipelines are singleton-safe and designed for concurrent use
Installation
NuGet Package Manager
Install-Package NetEvolve.Pulse.Polly
.NET CLI
dotnet add package NetEvolve.Pulse.Polly
PackageReference
<PackageReference Include="NetEvolve.Pulse.Polly" Version="x.x.x" />
Quick Start
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"));
Usage
Per-Handler Retry Policy
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;
}
})));
Circuit Breaker for External Dependencies
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;
}
})));
Combined Policies (Timeout + Retry + Circuit Breaker)
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 breaker
Void Commands
For 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))));
Event Handler Policies
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)
})));
⚠️ Warning: Event policies apply to all handlers for that event type. If the policy triggers a retry, all handlers will re-execute. Consider using IEventOutbox for reliable event delivery instead of aggressive retries.
Bulkhead Isolation
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
})));
Fallback Strategy
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" })
})));
Policy Execution Order
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
Best Practices
Retry Policies
- 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
Circuit Breakers
- 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
Timeouts
- 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
Bulkhead
- Use for resource-intensive operations (file processing, heavy computations)
- Set
PermitLimitbased on available resources (CPU cores, memory) - Monitor queue saturation for capacity planning
Performance
- 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
Events
- 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
Advanced Scenarios
Per-Handler Policy Configuration with Keyed Services
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();
});
Telemetry and Monitoring
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.
Comparison with Other Approaches
| 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 |
Requirements
- .NET 8.0, .NET 9.0, or .NET 10.0
- Polly v8.0 or later
Microsoft.Extensions.DependencyInjectionfor service registration
Contributing
Contributions are welcome! Please read the Contributing Guidelines before submitting a pull request.
Support
- Issues: Report bugs or request features on GitHub Issues
- Documentation: Read the full documentation at https://github.com/dailydevops/pulse
License
This project is licensed under the MIT License - see the LICENSE file for details.
Related Packages
- 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
Made with ❤️ by the NetEvolve Team
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.5)
- NetEvolve.Pulse.Extensibility (>= 0.10.11)
- Polly.Core (>= 8.6.6)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.5)
- NetEvolve.Pulse.Extensibility (>= 0.10.11)
- Polly.Core (>= 8.6.6)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.5)
- NetEvolve.Pulse.Extensibility (>= 0.10.11)
- Polly.Core (>= 8.6.6)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.