NetEvolve.Pulse.Polly
0.84.106
Prefix Reserved
dotnet add package NetEvolve.Pulse.Polly --version 0.84.106
NuGet\Install-Package NetEvolve.Pulse.Polly -Version 0.84.106
<PackageReference Include="NetEvolve.Pulse.Polly" Version="0.84.106" />
<PackageVersion Include="NetEvolve.Pulse.Polly" Version="0.84.106" />
<PackageReference Include="NetEvolve.Pulse.Polly" />
paket add NetEvolve.Pulse.Polly --version 0.84.106
#r "nuget: NetEvolve.Pulse.Polly, 0.84.106"
#:package NetEvolve.Pulse.Polly@0.84.106
#addin nuget:?package=NetEvolve.Pulse.Polly&version=0.84.106
#tool nuget:?package=NetEvolve.Pulse.Polly&version=0.84.106
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
IMediatorBuilder - Order-Aware: Follows Pulse's interceptor execution order (registration order, first registered is outermost) 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.Extensibility;
using Polly;
using Polly.Retry;
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
The snippets below assume the using directives from the Quick Start, plus Polly.CircuitBreaker and Polly.Fallback where those strategies are used.
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. Void is NetEvolve.Pulse.Extensibility.Void; alias it to avoid the ambiguity with System.Void:
using Void = NetEvolve.Pulse.Extensibility.Void;
services.AddPulse(config => config
.AddCommandHandler<DeleteOrderCommand, DeleteOrderHandler>()
.AddPollyCommandPolicies<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.
Stream Query Policies
Apply policies to streaming queries:
services.AddPulse(config => config
.AddStreamQueryHandler<GetOrdersStreamQuery, OrderDto, GetOrdersStreamQueryHandler>()
.AddPollyStreamQueryPolicies<GetOrdersStreamQuery, OrderDto>(pipeline => pipeline
.AddTimeout(TimeSpan.FromMinutes(2))));
The whole enumeration runs inside the pipeline, not only the call that opens the stream. Items are forwarded to the consumer as they are produced.
- Retry restarts the enumeration from the beginning. Items already sent to the consumer are not withdrawn, so the consumer may receive them again. Configure retries only for handlers whose enumeration is idempotent or fails before yielding items.
- Timeout limits how long the handler enumeration runs inside the pipeline. Because items pass through a single-slot buffer, time the consumer spends on an item counts toward the timeout while the handler is waiting to hand over the next item. Processing of the items still buffered or in progress when the handler finishes is not covered.
Bulkhead Isolation
Limit concurrent executions to prevent resource exhaustion. AddConcurrencyLimiter is provided by the separate Polly.RateLimiting package, and ConcurrencyLimiterOptions lives in System.Threading.RateLimiting:
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 registration order. The first registered interceptor is outermost and runs first. Plan your policy chain accordingly:
config
.AddCommandHandler<CreateOrder, Result, CreateOrderHandler>()
.AddActivityAndMetrics() // Executes first (outermost)
.AddPollyRequestPolicies<CreateOrder, Result>(...) // Executes second
.AddCommandInterceptor<CreateOrder, Result, ValidationInterceptor>(); // Executes third (innermost)
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
Each AddPolly*Policies<TRequest, ...> call registers its pipeline as a keyed service with the request Type as key, so every request type gets its own policy:
services.AddPulse(config => config
.AddPollyCommandPolicies<CreateCriticalOrderCommand, OrderResult>(pipeline => pipeline
.AddRetry(new RetryStrategyOptions<OrderResult> { MaxRetryAttempts = 5 }))
.AddPollyCommandPolicies<CreateOrderCommand, OrderResult>(pipeline => pipeline
.AddRetry(new RetryStrategyOptions<OrderResult> { MaxRetryAttempts = 2 })));
The interceptor resolves ResiliencePipeline<TResponse> keyed by typeof(TRequest) first, then falls back to an unkeyed ResiliencePipeline<TResponse>. If you register a pipeline yourself, the key must be the request Type; pipelines registered under other keys (for example strings) are ignored.
services.AddKeyedSingleton<ResiliencePipeline<OrderResult>>(
typeof(CreateCriticalOrderCommand),
(sp, key) => new ResiliencePipelineBuilder<OrderResult>()
.AddRetry(new RetryStrategyOptions<OrderResult> { MaxRetryAttempts = 5 })
.Build());
Telemetry and Monitoring
Metrics and logs for resilience strategies come from the separate Polly.Extensions package (this package only depends on Polly.Core). Enable them on the pipeline builder with ConfigureTelemetry; Polly then publishes all instruments under a single meter named Polly. See Polly telemetry for details.
// Requires the Polly.Extensions package and Microsoft.Extensions.Logging.
services.AddPulse(config => config
.AddPollyCommandPolicies<CreateOrderCommand, OrderResult>(pipeline => pipeline
.AddRetry(new RetryStrategyOptions<OrderResult>())
.ConfigureTelemetry(loggerFactory)));
// Example: listen to Polly metrics (requires System.Diagnostics.Metrics)
var meterListener = new MeterListener();
meterListener.InstrumentPublished = (instrument, listener) =>
{
if (instrument.Meter.Name == "Polly")
{
listener.EnableMeasurementEvents(instrument, null);
}
};
meterListener.Start();
When AddActivityAndMetrics() is registered before the Polly policies (so it is the outer interceptor), policy overhead such as retries is included in the measured handler execution time.
Comparison with Other Approaches
| Approach | Pros | Cons |
|---|---|---|
| Polly Interceptors | Declarative, reusable, testable, composable with other interceptors | Registration order 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.Configuration.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.14)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Options (>= 10.0.14)
- NetEvolve.Pulse.Extensibility (>= 0.84.106)
- Polly.Core (>= 8.9.0)
-
net8.0
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.14)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Options (>= 10.0.14)
- NetEvolve.Pulse.Extensibility (>= 0.84.106)
- Polly.Core (>= 8.9.0)
-
net9.0
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.14)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Options (>= 10.0.14)
- NetEvolve.Pulse.Extensibility (>= 0.84.106)
- Polly.Core (>= 8.9.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.