diff --git a/docs/dependency-injection.md b/docs/dependency-injection.md index 829ed5afae2..3a8969dbae6 100644 --- a/docs/dependency-injection.md +++ b/docs/dependency-injection.md @@ -1,3 +1,224 @@ -# Dependency Injection +# Dependency injection -🚧 This documentation is being written as part of the Polly v8 release. +Starting with version 8, Polly provides features that make the integration of Polly with the .NET [`IServiceCollection`](https://learn.microsoft.com/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection) Dependency Injection (DI) container more streamlined. This is a thin layer atop the [resilience pipeline registry](resilience-pipeline-registry.md) which manages resilience pipelines. + +## Usage + +To use the DI functionality, add the `Polly.Extensions` package to your project: + +```sh +dotnet add package Polly.Extensions +``` + +Afterwards, you can use the `AddResiliencePipeline(...)` extension method to set up your pipeline: + + +```cs +var services = new ServiceCollection(); + +// Define a resilience pipeline +services.AddResiliencePipeline("my-key", builder => +{ + // Add strategies to your pipeline here, timeout for example + builder.AddTimeout(TimeSpan.FromSeconds(10)); +}); + +// You can also access IServiceProvider by using the alternate overload +services.AddResiliencePipeline("my-key", (builder, context) => +{ + // Resolve any service from DI + var loggerFactory = context.ServiceProvider.GetRequiredService(); + + // Add strategies to your pipeline here + builder.AddTimeout(TimeSpan.FromSeconds(10)); +}); + +// Resolve the resilience pipeline +ServiceProvider serviceProvider = services.BuildServiceProvider(); +ResiliencePipelineProvider pipelineProvider = serviceProvider.GetRequiredService>(); +ResiliencePipeline pipeline = pipelineProvider.GetPipeline("my-key"); + +// Use it +await pipeline.ExecuteAsync(async cancellation => await Task.Delay(100, cancellation)); +``` + + +The `AddResiliencePipeline` extension method also registers the following services into the DI: + +- `ResiliencePipelineRegistry`: Allows adding and retrieving resilience pipelines. +- `ResiliencePipelineProvider`: Allows retrieving resilience pipelines. +- `IOptions>`: Options for `ResiliencePipelineRegistry`. + +> [!NOTE] The generic `string`` is inferred since the pipeline was defined using the "my-key" value. + +If you only need the registry without defining a pipeline, use the `AddResiliencePipelineRegistry(...)` method. + +### Generic resilience pipelines + +You can also define generic resilience pipelines (`ResiliencePipeline`), as demonstrated below: + + +```cs +var services = new ServiceCollection(); + +// Define a generic resilience pipeline +// First parameter is the type of key, second one is the type of the results the generic pipeline works with +services.AddResiliencePipeline("my-pipeline", builder => +{ + builder.AddRetry(new() + { + MaxRetryAttempts = 2, + ShouldHandle = new PredicateBuilder() + .Handle() + .Handle() + .HandleResult(response => response.StatusCode == System.Net.HttpStatusCode.InternalServerError) + }) + .AddTimeout(TimeSpan.FromSeconds(2)); +}); + +// Resolve the resilience pipeline +ServiceProvider serviceProvider = services.BuildServiceProvider(); +ResiliencePipelineProvider pipelineProvider = serviceProvider.GetRequiredService>(); +ResiliencePipeline pipeline = pipelineProvider.GetPipeline("my-key"); + +// Use it +await pipeline.ExecuteAsync( + async cancellation => await client.GetAsync(endpoint, cancellation), + cancellationToken); +``` + + +## Dynamic reloads + +Dynamic reloading is a feature of the pipeline registry that is also surfaced when using the `AddResiliencePipeline(...)` extension method. Use an overload that provides access to `AddResiliencePipelineContext`: + + +```cs +services + .Configure("my-retry-options", configurationSection) // Configure the options + .AddResiliencePipeline("my-pipeline", (builder, context) => + { + // Enable the reloads whenever the named options change + context.EnableReloads("my-retry-options"); + + // Utility method to retrieve the named options + var retryOptions = context.GetOptions("my-retry-options"); + + // Add retries using the resolved options + builder.AddRetry(retryOptions); + }); +``` + + +- `EnableReloads(...)` activates the dynamic reloading of `my-pipeline`. +- `RetryStrategyOptions` are fetched using `context.GetOptions(...)` utility method. +- A retry strategy is added. + +During a reload: + +- The callback re-executes. +- The previous pipeline is discarded. + +If an error occurs during reloading, the old pipeline remains, and dynamic reloading stops. + +## Resource disposal + +Like dynamic reloading, the pipeline registry's resource disposal feature lets you register callbacks. These callbacks run when the pipeline is discarded, reloaded, or the registry is disposed at application shutdown. + +See the example below: + + +```cs +services.AddResiliencePipeline("my-pipeline", (builder, context) => +{ + // Create disposable resource + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions { PermitLimit = 100, QueueLimit = 100 }); + + // Use it + builder.AddRateLimiter(limiter); + + // Dispose the resource created in the callback when the pipeline is discarded + context.OnPipelineDisposed(() => limiter.Dispose()); +}); +``` + + +This feature ensures that resources are properly disposed when a pipeline reloads, discarding the old version. + +## Complex pipeline keys + +The `AddResiliencePipeline(...)` method supports complex pipeline keys. This capability allows you to define the structure of your pipeline and dynamically resolve and cache multiple instances of the pipeline with different keys. + +Start by defining your complex key: + + +```cs +public record struct MyPipelineKey(string PipelineName, string InstanceName) +{ +} +``` + + +Next, register your pipeline: + + +```cs +services.AddResiliencePipeline(new MyPipelineKey("my-pipeline", string.Empty), builder => +{ + // Circuit breaker is a stateful strategy. To isolate the builder across different pipelines, + // we must use multiple instances. + builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions()); +}); +``` + + +The "my-pipeline" pipeline is now registered. Note that the `InstanceName` is an empty string. While we're registering the builder action for a specific pipeline, the `InstanceName` parameter isn't used during the pipeline's registration. Some further modifications are required for this to function. + +Introduce the `PipelineNameComparer`: + + +```cs +public sealed class PipelineNameComparer : IEqualityComparer +{ + public bool Equals(MyPipelineKey x, MyPipelineKey y) => x.PipelineName == y.PipelineName; + + public int GetHashCode(MyPipelineKey obj) => (obj.PipelineName, obj.InstanceName).GetHashCode(); +} +``` + + +Then, configure the registry behavior: + + +```cs +services + .AddResiliencePipelineRegistry(options => + { + options.BuilderComparer = new PipelineNameComparer(); + + options.InstanceNameFormatter = key => key.InstanceName; + + options.BuilderNameFormatter = key => key.PipelineName; + }); +``` + + +Let's summarize our actions: + +- We assigned the `PipelineNameComparer` instance to the `BuilderComparer` property. This action changes the default registry behavior, ensuring that only the `PipelineName` is used to find the associated builder. +- We used the `InstanceNameFormatter` delegate to represent the `MyPipelineKey` as an instance name for telemetry purposes, keeping the instance name as it is. +- Likewise, the `BuilderNameFormatter` delegate represents the `MyPipelineKey` as a builder name in telemetry. + +Finally, use the `ResiliencePipelineProvider` to dynamically create and cache multiple instances of the same pipeline: + + +```cs +ResiliencePipelineProvider pipelineProvider = serviceProvider.GetRequiredService>(); + +// The registry dynamically creates and caches instance-A using the associated builder action +ResiliencePipeline instanceA = pipelineProvider.GetPipeline(new MyPipelineKey("my-pipeline", "instance-A")); + +// The registry creates and caches instance-B +ResiliencePipeline instanceB = pipelineProvider.GetPipeline(new MyPipelineKey("my-pipeline", "instance-B")); +``` + diff --git a/docs/strategies/hedging.md b/docs/strategies/hedging.md index 1ab4e40593e..fb62d06216f 100644 --- a/docs/strategies/hedging.md +++ b/docs/strategies/hedging.md @@ -68,7 +68,7 @@ new ResiliencePipelineBuilder() | `DelayGenerator` | `null` | Used for generating custom delays for hedging. If `null` then `Delay` is used. | | `OnHedging` | `null` | Event that is raised when a hedging is performed. | -You can use the following special values for `Delay` or in `HedgingDelayGenerator`: +You can use the following special values for `Delay` or in `DelayGenerator`: - `0 seconds` - the hedging strategy immediately creates a total of `MaxHedgedAttempts` and completes when the fastest acceptable result is available. - `-1 millisecond` - this value indicates that the strategy does not create a new hedged task before the previous one completes. This enables scenarios where having multiple concurrent hedged tasks can cause side effects. @@ -109,10 +109,10 @@ The hedging strategy operates in parallel mode when the `Delay` property is set ### Dynamic mode -In dynamic mode, you have the flexibility to control how the hedging strategy behaves during each execution. This control is achieved through the `HedgingDelayGenerator` property. +In dynamic mode, you have the flexibility to control how the hedging strategy behaves during each execution. This control is achieved through the `DelayGenerator` property. > [!NOTE] -> The `Delay` property is disregarded when `HedgingDelayGenerator` is set. +> The `Delay` property is disregarded when `DelayGenerator` is set. Example scenario: @@ -166,7 +166,7 @@ new ResiliencePipelineBuilder() // Here, we can access the original callback and return it or return a completely new action var callback = args.Callback; - // A delegate that returns a ValueTask> is required. + // A function that returns a ValueTask> is required. return async () => { try diff --git a/src/Polly.Extensions/README.md b/src/Polly.Extensions/README.md index 9994d4ca2f5..30c6d0ce266 100644 --- a/src/Polly.Extensions/README.md +++ b/src/Polly.Extensions/README.md @@ -1,45 +1,6 @@ -# Polly.Extensions Overview +# Polly.Extensions overview -`Polly.Extensions` provides a set of features that streamline the integration of Polly with the standard `IServiceCollection` Dependency Injection (DI) container. It further enhances telemetry by exposing a `ConfigureTelemetry` extension method that enables [logging](https://learn.microsoft.com/dotnet/core/extensions/logging?tabs=command-line) and [metering](https://learn.microsoft.com/dotnet/core/diagnostics/metrics) for all strategies created via DI extension points. +This project provides the following features: -Below is an example illustrating the usage of `AddResiliencePipeline` extension method: - - -```cs -var services = new ServiceCollection(); - -// Define a resilience pipeline -services.AddResiliencePipeline( - "my-key", - builder => builder.AddTimeout(TimeSpan.FromSeconds(10))); - -// Define a resilience pipeline with custom options -services - .Configure(options => options.Timeout = TimeSpan.FromSeconds(10)) - .AddResiliencePipeline( - "my-timeout", - (builder, context) => - { - var myOptions = context.GetOptions(); - - builder.AddTimeout(myOptions.Timeout); - }); - -// Resolve the resilience pipeline -var serviceProvider = services.BuildServiceProvider(); -var pipelineProvider = serviceProvider.GetRequiredService>(); -var pipeline = pipelineProvider.GetPipeline("my-key"); - -// Use it -await pipeline.ExecuteAsync(async cancellation => await Task.Delay(100, cancellation)); -``` - - -> [!NOTE] -> Telemetry is enabled by default when utilizing the `AddResiliencePipeline(...)` extension method. - -## Telemetry Features - -This project implements the `TelemetryListener` and uses it to bridge the Polly-native events into logs and metrics. - -Explore [telemetry documentation](../../docs/telemetry.md) for more details. +- Incorporates [dependency injection](../../docs/dependency-injection.md) support and integrates with `IServiceCollection`. +- Offers [telemetry](../../docs/telemetry.md) support. This is achieved by implementing the `TelemetryListener` and utilizing it to translate the native Polly events into logs and metrics. diff --git a/src/Snippets/Docs/DependencyInjection.cs b/src/Snippets/Docs/DependencyInjection.cs new file mode 100644 index 00000000000..eed00eb3223 --- /dev/null +++ b/src/Snippets/Docs/DependencyInjection.cs @@ -0,0 +1,193 @@ +using System.Net.Http; +using System.Threading.RateLimiting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Polly; +using Polly.CircuitBreaker; +using Polly.Registry; +using Polly.Retry; +using Polly.Timeout; +using static Snippets.Docs.Telemetry; + +namespace Snippets.Docs; + +internal static class DependencyInjection +{ + public static async Task AddResiliencePipeline() + { + #region add-resilience-pipeline + + var services = new ServiceCollection(); + + // Define a resilience pipeline + services.AddResiliencePipeline("my-key", builder => + { + // Add strategies to your pipeline here, timeout for example + builder.AddTimeout(TimeSpan.FromSeconds(10)); + }); + + // You can also access IServiceProvider by using the alternate overload + services.AddResiliencePipeline("my-key", (builder, context) => + { + // Resolve any service from DI + var loggerFactory = context.ServiceProvider.GetRequiredService(); + + // Add strategies to your pipeline here + builder.AddTimeout(TimeSpan.FromSeconds(10)); + }); + + // Resolve the resilience pipeline + ServiceProvider serviceProvider = services.BuildServiceProvider(); + ResiliencePipelineProvider pipelineProvider = serviceProvider.GetRequiredService>(); + ResiliencePipeline pipeline = pipelineProvider.GetPipeline("my-key"); + + // Use it + await pipeline.ExecuteAsync(async cancellation => await Task.Delay(100, cancellation)); + + #endregion + } + + public static async Task AddResiliencePipelineGeneric() + { + using var client = new HttpClient(); + var endpoint = new Uri("https://www.dummy.com"); + var cancellationToken = CancellationToken.None; + + #region add-resilience-pipeline-generic + + var services = new ServiceCollection(); + + // Define a generic resilience pipeline + // First parameter is the type of key, second one is the type of the results the generic pipeline works with + services.AddResiliencePipeline("my-pipeline", builder => + { + builder.AddRetry(new() + { + MaxRetryAttempts = 2, + ShouldHandle = new PredicateBuilder() + .Handle() + .Handle() + .HandleResult(response => response.StatusCode == System.Net.HttpStatusCode.InternalServerError) + }) + .AddTimeout(TimeSpan.FromSeconds(2)); + }); + + // Resolve the resilience pipeline + ServiceProvider serviceProvider = services.BuildServiceProvider(); + ResiliencePipelineProvider pipelineProvider = serviceProvider.GetRequiredService>(); + ResiliencePipeline pipeline = pipelineProvider.GetPipeline("my-key"); + + // Use it + await pipeline.ExecuteAsync( + async cancellation => await client.GetAsync(endpoint, cancellation), + cancellationToken); + + #endregion + } + + public static async Task DynamicReloads(IServiceCollection services, IConfigurationSection configurationSection) + { + #region di-dynamic-reloads + + services + .Configure("my-retry-options", configurationSection) // Configure the options + .AddResiliencePipeline("my-pipeline", (builder, context) => + { + // Enable the reloads whenever the named options change + context.EnableReloads("my-retry-options"); + + // Utility method to retrieve the named options + var retryOptions = context.GetOptions("my-retry-options"); + + // Add retries using the resolved options + builder.AddRetry(retryOptions); + }); + + #endregion + } + + public static async Task ResourceDisposal(IServiceCollection services) + { + #region di-resource-disposal + + services.AddResiliencePipeline("my-pipeline", (builder, context) => + { + // Create disposable resource + var limiter = new ConcurrencyLimiter(new ConcurrencyLimiterOptions { PermitLimit = 100, QueueLimit = 100 }); + + // Use it + builder.AddRateLimiter(limiter); + + // Dispose the resource created in the callback when the pipeline is discarded + context.OnPipelineDisposed(() => limiter.Dispose()); + }); + + #endregion + } + + #region di-registry-complex-key + + public record struct MyPipelineKey(string PipelineName, string InstanceName) + { + } + + #endregion + + public static async Task AddResiliencePipelineWithComplexKey(IServiceCollection services) + { + #region di-registry-add-pipeline + + services.AddResiliencePipeline(new MyPipelineKey("my-pipeline", string.Empty), builder => + { + // Circuit breaker is a stateful strategy. To isolate the builder across different pipelines, + // we must use multiple instances. + builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions()); + }); + + #endregion + } + + #region di-complex-key-comparer + + public sealed class PipelineNameComparer : IEqualityComparer + { + public bool Equals(MyPipelineKey x, MyPipelineKey y) => x.PipelineName == y.PipelineName; + + public int GetHashCode(MyPipelineKey obj) => (obj.PipelineName, obj.InstanceName).GetHashCode(); + } + + #endregion + + public static async Task ConfigureRegistry(IServiceCollection services) + { + #region di-registry-configure + + services + .AddResiliencePipelineRegistry(options => + { + options.BuilderComparer = new PipelineNameComparer(); + + options.InstanceNameFormatter = key => key.InstanceName; + + options.BuilderNameFormatter = key => key.PipelineName; + }); + + #endregion + } + + public static async Task ConfigureRegistry(IServiceProvider serviceProvider) + { + #region di-registry-multiple-instances + + ResiliencePipelineProvider pipelineProvider = serviceProvider.GetRequiredService>(); + + // The registry dynamically creates and caches instance-A using the associated builder action + ResiliencePipeline instanceA = pipelineProvider.GetPipeline(new MyPipelineKey("my-pipeline", "instance-A")); + + // The registry creates and caches instance-B + ResiliencePipeline instanceB = pipelineProvider.GetPipeline(new MyPipelineKey("my-pipeline", "instance-B")); + + #endregion + } +} diff --git a/src/Snippets/Docs/Hedging.cs b/src/Snippets/Docs/Hedging.cs index 2912e392471..b57bd303c9c 100644 --- a/src/Snippets/Docs/Hedging.cs +++ b/src/Snippets/Docs/Hedging.cs @@ -101,7 +101,7 @@ public static void ActionGenerator() { try { - // A dedicated ActionContext is provided for each hedged action + // A dedicated ActionContext is provided for each hedged action. // It comes with a separate CancellationToken created specifically for this hedged attempt, // which can be cancelled later if needed. // diff --git a/src/Snippets/Docs/Telemetry.cs b/src/Snippets/Docs/Telemetry.cs index 17bc4dbabf2..afa079efedf 100644 --- a/src/Snippets/Docs/Telemetry.cs +++ b/src/Snippets/Docs/Telemetry.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Polly; using Polly.Retry; using Polly.Telemetry; @@ -48,6 +49,25 @@ public static void ConfigureTelemetry() #endregion } + public static void AddResiliencePipelineWithTelemetry() + { + #region add-resilience-pipeline-with-telemetry + + var serviceCollection = new ServiceCollection() + .AddLogging(builder => builder.AddConsole()) + .AddResiliencePipeline("my-strategy", builder => builder.AddTimeout(TimeSpan.FromSeconds(1))) + .Configure(options => + { + // Configure enrichers + options.MeteringEnrichers.Add(new MyMeteringEnricher()); + + // Configure telemetry listeners + options.TelemetryListeners.Add(new MyTelemetryListener()); + }); + + #endregion + } + #region telemetry-listeners internal class MyTelemetryListener : TelemetryListener diff --git a/src/Snippets/Extensions/Snippets.cs b/src/Snippets/Extensions/Snippets.cs deleted file mode 100644 index e0f0e420f92..00000000000 --- a/src/Snippets/Extensions/Snippets.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Polly; -using Polly.Registry; -using Polly.Telemetry; -using static Snippets.Docs.Telemetry; - -namespace Snippets.Extensions; - -internal static class Snippets -{ - public static async Task AddResiliencePipeline() - { - #region add-resilience-pipeline - - var services = new ServiceCollection(); - - // Define a resilience pipeline - services.AddResiliencePipeline( - "my-key", - builder => builder.AddTimeout(TimeSpan.FromSeconds(10))); - - // Define a resilience pipeline with custom options - services - .Configure(options => options.Timeout = TimeSpan.FromSeconds(10)) - .AddResiliencePipeline( - "my-timeout", - (builder, context) => - { - var myOptions = context.GetOptions(); - - builder.AddTimeout(myOptions.Timeout); - }); - - // Resolve the resilience pipeline - var serviceProvider = services.BuildServiceProvider(); - var pipelineProvider = serviceProvider.GetRequiredService>(); - var pipeline = pipelineProvider.GetPipeline("my-key"); - - // Use it - await pipeline.ExecuteAsync(async cancellation => await Task.Delay(100, cancellation)); - - #endregion - } - - public static void AddResiliencePipelineWithTelemetry() - { - #region add-resilience-pipeline-with-telemetry - - var serviceCollection = new ServiceCollection() - .AddLogging(builder => builder.AddConsole()) - .AddResiliencePipeline("my-strategy", builder => builder.AddTimeout(TimeSpan.FromSeconds(1))) - .Configure(options => - { - // Configure enrichers - options.MeteringEnrichers.Add(new MyMeteringEnricher()); - - // Configure telemetry listeners - options.TelemetryListeners.Add(new MyTelemetryListener()); - }); - - #endregion - } - - private class MyTimeoutOptions - { - public TimeSpan Timeout { get; set; } - } -}