HomeNikola Knezevic

In this article

Banner

Resilient .NET Application with Polly Resilience Pipelines

27 Dec 2024
6 min

Happy New Year! 🎉

With the arrival of 2025, I’d like to take a moment to wish you a year filled with success, growth and new possibilities.

Thank you for being a part of this journey. I look forward to sharing more insights, stories and exciting content with you throughout the year.

Let’s make 2025 our best year yet. Happy reading!

Resilience

Building resilient applications means handling failures gracefully to ensure they continue functioning smoothly despite errors or unexpected behavior.

This is especially crucial in distributed systems, where failures are common due to their complexity and dependencies.

By implementing resilience strategies, developers can keep applications functional, performant, and reliable even in the face of unforeseen issues.

Polly

Polly is a powerful library for .NET that helps you handle transient faults and improve the resilience of your applications.

With Polly, you can easily define and apply different resilient strategies, which are categorized into two main groups:

Reactive - These strategies handle specific exceptions or results by the callbacks executed within the strategy.

  • Retry - Attempt the operation again if it fails, can be useful when the issue is temporary and likely to resolve itself.
  • Circuit-breaker - Stop attempting if something is broken or overloaded, helping prevent wasted time while allowing the system to recover.
  • Fallback - Perform an alternative action if something fails, enhancing user experience and ensuring everything continues to function.
  • Hedging - Execute multiple actions simultaneously and use the fastest result, improving speed and responsiveness.

Proactive - Unlike reactive strategies, proactive strategies focus on preventing errors by canceling or rejecting callback executions, rather than handling errors after they occur.

  • Timeout - Abort if something takes too long, helping improve performance by freeing up resources and space.
  • Rate Limiter - Restrict the number of requests you make or accept, helping control load and avoid issues or penalties.

Getting started

In this blog, instead of using Polly directly, we will use the Microsoft.Extensions.Resilience and Microsoft.Extensions.Http.Resilience packages, which are built on top of Polly.

To get started, you need to install the NuGet packages. You can do this via the NuGet Package Manager or by running the following command in the Package Manager Console:

bash
Install-Package Microsoft.Extensions.Resilience
Install-Package Microsoft.Extensions.Http.Resilience

Resilience Pipelines

To implement resilience, you need to build a pipeline of resilience-based strategies. Each strategy runs in the order it is set up, so the sequence is important.

To begin, create an instance of ResiliencePipelineBuilder, which serves as the starting point for configuring and combining resilience strategies into a unified pipeline.

To add a strategy to the pipeline, use one of the available Add* extension methods on the ResiliencePipelineBuilder instance.

Once you add desired strategies, use the Build method to create a configured ResiliencePipeline instance that applies these strategies.

csharp
var pipeline = new ResiliencePipelineBuilder<CurrencyLimitResponse?>()
    .AddFallback(new FallbackStrategyOptions<CurrencyLimitResponse?>
    {
        FallbackAction = _ => Outcome.FromResultAsValueTask<CurrencyLimitResponse?>(
            new CurrencyLimitResponse())
    })
    .Build();

In this example, we added a fallback strategy using the AddFallback method and provided the appropriate options to define the outcome in case of a failure.

This approach ensures the application can recover gracefully by substituting a default or previously cached response when an operation fails.

To execute the pipeline, call the ExecuteAsync method, passing in the desired delegate:

csharp
var response = await pipeline.ExecuteAsync(
    async ct => await cryptoApi.GetLimits(ct),
    cancellationToken);

However, realistically, no one would want to create and configure a pipeline every time one is needed, which is why it's better to register resilience pipelines with dependency injection.

To register a pipeline, you can configure it for a specific key and use that key to retrieve the corresponding resilience pipeline from the provider.

csharp
builder.Services.AddResiliencePipeline("retry", pipelineBuilder =>
{
    pipelineBuilder.AddRetry(new RetryStrategyOptions
    {
        ShouldHandle = new PredicateBuilder().Handle<Exception>(),
        Delay = TimeSpan.FromSeconds(2),
        MaxRetryAttempts = 3,
        UseJitter = true,
        OnRetry = args =>
        {
            Console.WriteLine($"Attempt: {args.AttemptNumber}");
            return ValueTask.CompletedTask;
        }
    });
});

To use resilience pipelines configured with dependency injection, we can utilize the ResiliencePipelineProvider, which provides a GetPipeline method to retrieve the pipeline instance using the key provided during registration.

csharp
app.MapGet("crypto/limits", async (
    CryptoApiClient cryptoApi,
    ResiliencePipelineProvider<string> pipelineProvider,
    CancellationToken cancellationToken) =>
{
    ResiliencePipeline pipeline = pipelineProvider.GetPipeline("retry");
    var response = await pipeline.ExecuteAsync(
        async ct => await cryptoApi.GetLimits(ct),
        cancellationToken);
    return Results.Ok(response);
});

Http Client

Building resilient HTTP apps that can recover from transient errors is a common task.

To assist in this, the Microsoft.Extensions.Http.Resilience NuGet package provides resilience mechanisms specifically for HttpClient. You can add resilience to an HttpClient by chaining it with the AddHttpClient method.

The library offers predefined handlers like AddStandardResilienceHandler and AddStandardHedgingHandler, which follow various industry best practices.

csharp
builder.Services.AddHttpClient<CryptoApiClient>(client =>
        client.BaseAddress = new Uri("https://cex.io/api/"))
    .AddStandardResilienceHandler();
    
builder.Services.AddHttpClient<NikolaTechApiClient>(client =>
        client.BaseAddress = new Uri("https://nikolatech.net/"))
    .AddStandardHedgingHandler();

These handlers implement five different strategies, and you can customize their default settings to adjust their behavior.

It’s recommended to add only one resilience handler and avoid stacking multiple handlers. If you need more than one, consider using the AddResilienceHandler extension method, which lets you customize the resilience strategies to meet your needs.

Conclusion

With resilient apps, you can relax and fully enjoy your holidays without any worries.

The ability to automatically recover from faults leads to better system performance and reduced operational overhead. Additionally, as systems grow more complex, having resilience mechanisms in place becomes essential to manage the increasing volume of requests and errors effectively.

By implementing resilience pipelines, you can ensure your applications continue to run seamlessly.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.