Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Docs] Dependency injection #1564

Merged
merged 3 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 223 additions & 2 deletions docs/dependency-injection.md
Original file line number Diff line number Diff line change
@@ -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:

<!-- snippet: add-resilience-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<ILoggerFactory>();

// Add strategies to your pipeline here
builder.AddTimeout(TimeSpan.FromSeconds(10));
});

// Resolve the resilience pipeline
ServiceProvider serviceProvider = services.BuildServiceProvider();
ResiliencePipelineProvider<string> pipelineProvider = serviceProvider.GetRequiredService<ResiliencePipelineProvider<string>>();
ResiliencePipeline pipeline = pipelineProvider.GetPipeline("my-key");

// Use it
await pipeline.ExecuteAsync(async cancellation => await Task.Delay(100, cancellation));
```
<!-- endSnippet -->

The `AddResiliencePipeline` extension method also registers the following services into the DI:

- `ResiliencePipelineRegistry<string>`: Allows adding and retrieving resilience pipelines.
- `ResiliencePipelineProvider<string>`: Allows retrieving resilience pipelines.
- `IOptions<ResiliencePipelineRegistryOptions<string>>`: Options for `ResiliencePipelineRegistry<string>`.

> [!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<T>`), as demonstrated below:

<!-- snippet: add-resilience-pipeline-generic -->
```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<string, HttpResponseMessage>("my-pipeline", builder =>
{
builder.AddRetry(new()
{
MaxRetryAttempts = 2,
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.Handle<TimeoutRejectedException>()
.HandleResult(response => response.StatusCode == System.Net.HttpStatusCode.InternalServerError)
})
.AddTimeout(TimeSpan.FromSeconds(2));
});

// Resolve the resilience pipeline
ServiceProvider serviceProvider = services.BuildServiceProvider();
ResiliencePipelineProvider<string> pipelineProvider = serviceProvider.GetRequiredService<ResiliencePipelineProvider<string>>();
ResiliencePipeline<HttpResponseMessage> pipeline = pipelineProvider.GetPipeline<HttpResponseMessage>("my-key");

// Use it
await pipeline.ExecuteAsync(
async cancellation => await client.GetAsync(endpoint, cancellation),
cancellationToken);
```
<!-- endSnippet -->

## 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`:

<!-- snippet: di-dynamic-reloads -->
```cs
services
.Configure<RetryStrategyOptions>("my-retry-options", configurationSection) // Configure the options
.AddResiliencePipeline("my-pipeline", (builder, context) =>
{
// Enable the reloads whenever the named options change
context.EnableReloads<RetryStrategyOptions>("my-retry-options");

// Utility method to retrieve the named options
var retryOptions = context.GetOptions<RetryStrategyOptions>("my-retry-options");

// Add retries using the resolved options
builder.AddRetry(retryOptions);
});
```
<!-- endSnippet -->

- `EnableReloads<T>(...)` 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:

<!-- snippet: di-resource-disposal -->
```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());
});
```
<!-- endSnippet -->

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:

<!-- snippet: di-registry-complex-key -->
```cs
public record struct MyPipelineKey(string PipelineName, string InstanceName)
{
}
```
<!-- endSnippet -->

Next, register your pipeline:

<!-- snippet: di-registry-add-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());
});
```
<!-- endSnippet -->

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`:

<!-- snippet: di-complex-key-comparer -->
```cs
public sealed class PipelineNameComparer : IEqualityComparer<MyPipelineKey>
{
public bool Equals(MyPipelineKey x, MyPipelineKey y) => x.PipelineName == y.PipelineName;

public int GetHashCode(MyPipelineKey obj) => (obj.PipelineName, obj.InstanceName).GetHashCode();
}
```
<!-- endSnippet -->

Then, configure the registry behavior:

<!-- snippet: di-registry-configure -->
```cs
services
.AddResiliencePipelineRegistry<MyPipelineKey>(options =>
{
options.BuilderComparer = new PipelineNameComparer();

options.InstanceNameFormatter = key => key.InstanceName;

options.BuilderNameFormatter = key => key.PipelineName;
});
```
<!-- endSnippet -->

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<MyPipelineKey>` to dynamically create and cache multiple instances of the same pipeline:

<!-- snippet: di-registry-multiple-instances -->
```cs
ResiliencePipelineProvider<MyPipelineKey> pipelineProvider = serviceProvider.GetRequiredService<ResiliencePipelineProvider<MyPipelineKey>>();

// 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"));
```
<!-- endSnippet -->
8 changes: 4 additions & 4 deletions docs/strategies/hedging.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ new ResiliencePipelineBuilder<HttpResponseMessage>()
| `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.
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -166,7 +166,7 @@ new ResiliencePipelineBuilder<HttpResponseMessage>()
// 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<Outcome<HttpResponseMessage>> is required.
// A function that returns a ValueTask<Outcome<HttpResponseMessage>> is required.
return async () =>
{
try
Expand Down
47 changes: 4 additions & 43 deletions src/Polly.Extensions/README.md
Original file line number Diff line number Diff line change
@@ -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:

<!-- snippet: add-resilience-pipeline -->
```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<MyTimeoutOptions>(options => options.Timeout = TimeSpan.FromSeconds(10))
.AddResiliencePipeline(
"my-timeout",
(builder, context) =>
{
var myOptions = context.GetOptions<MyTimeoutOptions>();

builder.AddTimeout(myOptions.Timeout);
});

// Resolve the resilience pipeline
var serviceProvider = services.BuildServiceProvider();
var pipelineProvider = serviceProvider.GetRequiredService<ResiliencePipelineProvider<string>>();
var pipeline = pipelineProvider.GetPipeline("my-key");

// Use it
await pipeline.ExecuteAsync(async cancellation => await Task.Delay(100, cancellation));
```
<!-- endSnippet -->

> [!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.
Loading