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
                    
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.10.11" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NetEvolve.Pulse.Polly" Version="0.10.11" />
                    
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.10.11
                    
#r "nuget: NetEvolve.Pulse.Polly, 0.10.11"
                    
#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.10.11
                    
#: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.10.11
                    
Install as a Cake Addin
#tool nuget:?package=NetEvolve.Pulse.Polly&version=0.10.11
                    
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 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 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 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.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.10.11 3 3/27/2026
0.10.5 42 3/27/2026
0.7.1 65 3/25/2026