Polly

Resilience pipelines

[!NOTE] This is documentation for the upcoming Polly v8 release.

The ResiliencePipeline allows executing arbitrary user-provided callbacks. It is a combination of one or more resilience strategies.

Usage

The ResiliencePipeline allow executing various synchronous and asynchronous user-provided callbacks as seen in the examples below:

// Creating a new resilience pipeline
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
    .AddConcurrencyLimiter(100)
    .Build();

// Executing an asynchronous void callback
await pipeline.ExecuteAsync(
    async token => await MyMethodAsync(token),
    cancellationToken);

// Executing a synchronous void callback
pipeline.Execute(() => MyMethod());

// Executing an asynchronous callback that returns a value
await pipeline.ExecuteAsync(
    async token => await httpClient.GetAsync(endpoint, token),
    cancellationToken);

// Executing an asynchronous callback without allocating a lambda
await pipeline.ExecuteAsync(
    static async (state, token) => await state.httpClient.GetAsync(state.endpoint, token),
    (httpClient, endpoint),  // State provided here
    cancellationToken);

// Executing an asynchronous callback and passing custom data

// 1. Retrieve a context from the shared pool
ResilienceContext context = ResilienceContextPool.Shared.Get(cancellationToken);

// 2. Add custom data to the context
context.Properties.Set(new ResiliencePropertyKey<string>("my-custom-data"), "my-custom-data");

// 3. Execute the callback
await pipeline.ExecuteAsync(static async context =>
{
    // Retrieve custom data from the context
    var customData = context.Properties.GetValue(
        new ResiliencePropertyKey<string>("my-custom-data"),
        "default-value");

    Console.WriteLine("Custom Data: {0}", customData);

    await MyMethodAsync(context.CancellationToken);
},
context);

// 4. Optionally, return the context to the shared pool
ResilienceContextPool.Shared.Return(context);

The above samples demonstrate how to use the resilience pipeline within the same scope. Additionally, consider the following:

public static void ConfigureMyPipelines(IServiceCollection services)
{
    services.AddResiliencePipeline("pipeline-A", builder => builder.AddConcurrencyLimiter(100));
    services.AddResiliencePipeline("pipeline-B", builder => builder.AddRetry(new()));

    // Later, resolve the pipeline by name using ResiliencePipelineProvider<string> or ResiliencePipelineRegistry<string>
    var pipelineProvider = services.BuildServiceProvider().GetRequiredService<ResiliencePipelineProvider<string>>();
    pipelineProvider.GetPipeline("pipeline-A").Execute(() => { });
}

Empty resilience pipeline

The empty resilience pipeline is a special construct that lacks any resilience strategies. You can access it through the following ways:

This is particularly useful in test scenarios where implementing resilience strategies could slow down the test execution or over-complicate test setup.

Retrieving execution results with Outcome<T>

The ResiliencePipeline class provides the ExecuteOutcomeAsync(...) method, which is designed to never throw exceptions. Instead, it stores either the result or the exception within an Outcome<T> struct.

// Acquire a ResilienceContext from the pool
ResilienceContext context = ResilienceContextPool.Shared.Get();

// Execute the pipeline and store the result in an Outcome<bool>
Outcome<bool> outcome = await pipeline.ExecuteOutcomeAsync(
    static async (context, state) =>
    {
        Console.WriteLine("State: {0}", state);

        try
        {
            await MyMethodAsync(context.CancellationToken);

            // Use static utility methods from Outcome to easily create an Outcome<T> instance
            return Outcome.FromResult(true);
        }
        catch (Exception e)
        {
            // Create an Outcome<T> instance that holds the exception
            return Outcome.FromException<bool>(e);
        }
    },
    context,
    "my-state");

// Return the acquired ResilienceContext to the pool
ResilienceContextPool.Shared.Return(context);

// Evaluate the outcome
if (outcome.Exception is not null)
{
    Console.WriteLine("Execution Failed: {0}", outcome.Exception.Message);
}
else
{
    Console.WriteLine("Execution Result: {0}", outcome.Result);
}

Use ExecuteOutcomeAsync(...) in high-performance scenarios where you wish to avoid re-throwing exceptions. Keep in mind that Polly’s resilience strategies also make use of the Outcome struct to prevent unnecessary exception throwing.