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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NetEvolve.Pulse.Polly" Version="0.84.106" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NetEvolve.Pulse.Polly" Version="0.84.106" />
                    
Directory.Packages.props
<PackageReference Include="NetEvolve.Pulse.Polly" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NetEvolve.Pulse.Polly --version 0.84.106
                    
#r "nuget: NetEvolve.Pulse.Polly, 0.84.106"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NetEvolve.Pulse.Polly@0.84.106
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NetEvolve.Pulse.Polly&version=0.84.106
                    
Install as a Cake Addin
#tool nuget:?package=NetEvolve.Pulse.Polly&version=0.84.106
                    
Install as a Cake Tool

NetEvolve.Pulse.Polly

NuGet Version NuGet Downloads License

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 MaxRetryAttempts conservative (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) and MinimumThroughput values
  • 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 PermitLimit based 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 IEventOutbox pattern 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.DependencyInjection for service registration

Contributing

Contributions are welcome! Please read the Contributing Guidelines before submitting a pull request.

Support

License

This project is licensed under the MIT License - see the LICENSE file for details.


Made with ❤️ by the NetEvolve Team

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.84.106 143 11/8/2026
0.70.75 1,611 7/28/2026
0.70.16 404 7/27/2026
0.70.0 200 7/24/2026
0.67.140 161 7/23/2026
0.67.36 701 5/10/2026
0.17.3 1,127 3/28/2026
0.10.11 279 3/27/2026
0.10.5 156 3/27/2026
0.7.1 167 3/25/2026