When developing an application, there’s many things that can go wrong in your code. One of the problems you need to deal with is when an external system is sensitive to overload or can drop off unexpectedly. These “problems” can be broadly classified under the banner of transient faults. Polly is a library you can use to assist with developing applications which rely on external systems. Link to all code is available via the buttons below.

Link to Polly Project

Dealing with Transient Faults without Polly

Suppose that we’re developing a program which relies on an external system which is unreliable. How would you go about doing a “roll your own” implementation? In the example you’ll see an example where we create our own implementation when connecting to the bored api client.

app.MapGet("/custom", async ([FromServices] IHttpClientFactory clientFactory) =>
{
    var client = clientFactory.CreateClient("boredapi");
    var retryCount = 0;
    Start:
    try
    {
        var response = await client.GetAsync("activity");
        if (response.StatusCode is >= HttpStatusCode.InternalServerError or HttpStatusCode.RequestTimeout)
        {
            //i.e. we're dealing with a transient error
            if (retryCount>=5)
            {
                return Results.BadRequest("Server Error - Too many retries attempted.");
            }
            retryCount++;
            goto Start;    
        }

        return Results.Ok(await response.Content.ReadAsStringAsync());

    }
    catch (HttpRequestException)
    {
        if (retryCount>=5)
        {
            return Results.BadRequest("Server Error - Too many retries attempted.");
        }
        retryCount++;
        goto Start;
    }

    return Results.BadRequest("This is awkward");
});

Off the bat - this code isn’t great. In general goto statements are considered an anti-pattern - with your code jumping all over the place it’s difficult to debug amongst other problems. Secondly, having your own bespoke retry functionality can get quite messy as the problem scope becomes more complex. Let’s take a look at what Polly offers.

Retries with Polly

Polly uses something called policies which define how your application should behave under various circumstances. In this example we’d like to try the api endpoint 5 times before giving up.

private static readonly IAsyncPolicy<HttpResponseMessage> _retryPolicy =
    Policy<HttpResponseMessage>
        .Handle<HttpRequestException>()
        .OrResult(response =>
            response.StatusCode is >= HttpStatusCode.InternalServerError or HttpStatusCode.RequestTimeout)
        //retry up to 5 times
        .RetryAsync(5);

public static WebApplication AddRetryEndpoints(this WebApplication app)
{
    app.MapGet("/Retry", async ([FromServices] IHttpClientFactory clientFactory) =>
    {
        var client = clientFactory.CreateClient("boredapi");
        var response = 
            await _retryPolicy
                .ExecuteAsync(() => client.GetAsync("activity"));
        return response.Content.ReadAsStringAsync();

    });
        
    return app;
}

In the above code we can handle specific exceptions that you can specify or specific results. What’s great about this is how you can go about defining a retry policy once and in theory reuse it for several scenarios if needed.

Caching with Polly

Similarly, if you have a scenario in which caching results from previous requests is fine within some bounded time frame, Polly can assist with the implementation.

MemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions());
MemoryCacheProvider memoryCacheProvider = new MemoryCacheProvider(memoryCache);
var cachePolicy = Policy.CacheAsync(memoryCacheProvider, TimeSpan.FromMinutes(1));


app.MapGet("/Caching", async ([FromServices] IHttpClientFactory clientFactory) =>
{
    var getResult = async () =>
    {
        var client = clientFactory.CreateClient("boredapi");
        
        var result =  await client.GetAsync("activity");
        return await result.Content.ReadAsStringAsync();
    };
    var result = await cachePolicy.ExecuteAsync(context => getResult() , new Context("FooKey") );
    return Results.Ok(result);
});

In the above code an InMemory cache is created which is valid for one minute. Hitting this endpoint twice in a minute will result in a previously retrieved result being returned. What I find particularly cool about this is that you’re not limited to in memory cache’s. Other caching mechanisms like Redis or even SQL server can be used.

Conclusion

The above code is really just a sample of what can be achieved with Polly. Other problems you might want to solve with Polly include

  • rate limiting for incoming requests
  • exponential backoffs (with and without Jitter) when the system you’re trying to communicate with isn’t responding.

The Polly library is really great since it allows you to standardize how you interact with various systems and how you respond to incoming requests on your system. It protects both the systems you use as well as your own system.

Additional Resources

  • https://www.youtube.com/watch?v=nJH0PC2Pubs