https://www.pollydocs.org/
Important note about Polly library
The Polly library has changed its API quite a bit from v7 to v8 and now to v9. While Polly has been a Microsoft Community project, Microsoft now takes a bit more lead with Polly v9 being more tightly coupled to Microsoft's ecosystem. Future v10 is expected to be not so much changed as earlier major versions of Polly, only stabilize andimprove. The following Nuget libraries are added to the demo solution, SwashBuckle Nuget also added here for Swagger support used when testing.
<ItemGroup> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.7" /> <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" /> </ItemGroup>
The source code shown in this article is available to clone from my Github repo here: https://github.com/toreaurstadboss/HttpClientUsingPolly
Extension methods to add Polly v9 support http clients
The following extension methods offers creating a HttpClient via IHttpClientFactory or via a pipeline provided by ResiliencyProvider. Note that only the retry resilience strategy is shown here. There are multiple strategies available with Polly.PollyExtensions.cs
using Microsoft.Extensions.Http.Resilience;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.Retry;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
namespace HttpClientUsingPolly
{
/// <summary>
/// Contains helper methods to add support for Polly V9 resilience strategies
/// </summary>
public static class PollyExtensions
{
public static void AddPollyHttpClient(this IServiceCollection services)
{
services.AddHttpClient(GithubEndpoints.HttpClientName, client =>
{
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new RandomHttpErrorHandler(errorChance: 75);
handler.InnerHandler = new HttpClientHandler(); // Assign the terminal handler
return handler;
}) //IMPORTANT to make Polly retry here using ConfigurePrimaryHttpMessageHandler and set the Innerhandler to HttpClientHandler
.AddResilienceHandler(RetryResiliencePolicy, builder =>
{
var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("NamedPolly");
var options = CreateRetryStrategyOptions(logger);
builder.AddRetry(options);
});
}
public static void AddNamedPollyPipelines(this IServiceCollection services)
{
services.AddResiliencePipeline<string>(RetryResiliencePolicy, builder =>
{
var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("NamedPolly");
builder.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
DelayGenerator = RetryDelaysClient,
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
OnRetry = args =>
{
var httpEx = args.Outcome.Exception as HttpRequestException;
logger.LogInformation($"[NamedPolicy] Retrying due to: {httpEx?.Message}. Attempt: {args.AttemptNumber}");
return default;
}
});
});
}
private static HttpRetryStrategyOptions CreateRetryStrategyOptions(ILogger logger)
{
HttpRetryStrategyOptions options = new HttpRetryStrategyOptions
{
MaxRetryAttempts = 3,
DelayGenerator = RetryDelaysPipeline,
ShouldHandle = args =>
{
if (args.Outcome.Exception is HttpRequestException ||
args.Outcome.Exception is TaskCanceledException)
return ValueTask.FromResult(true);
if (args.Outcome.Result is HttpResponseMessage response &&
TransientStatusCodes.Contains(response.StatusCode))
return ValueTask.FromResult(true);
return ValueTask.FromResult(false);
},
OnRetry = args =>
{
logger.LogInformation($"Retrying... Attempt {args.AttemptNumber}");
return default;
}
};
return options;
}
public const string RetryResiliencePolicy = "RetryResiliencePolicy";
static readonly HttpStatusCode[] TransientStatusCodes = new[]
{
HttpStatusCode.RequestTimeout, // 408
HttpStatusCode.InternalServerError, // 500
HttpStatusCode.BadGateway, // 502
HttpStatusCode.ServiceUnavailable, // 503
HttpStatusCode.GatewayTimeout // 504
};
static Func<RetryDelayGeneratorArguments<HttpResponseMessage>, ValueTask<TimeSpan?>>? RetryDelaysPipeline =
args => CommonDelayGenerator(args.AttemptNumber);
static Func<RetryDelayGeneratorArguments<object>, ValueTask<TimeSpan?>>? RetryDelaysClient =
args => CommonDelayGenerator(args.AttemptNumber);
static ValueTask<TimeSpan?> CommonDelayGenerator(int attemptNumber)
{
var delay = attemptNumber switch
{
1 => TimeSpan.FromSeconds(1),
2 => TimeSpan.FromSeconds(2),
3 => TimeSpan.FromSeconds(4),
_ => TimeSpan.FromSeconds(0) // fallback, shouldn't hit
};
return new ValueTask<TimeSpan?>(delay);
}
}
}
Note that the use of ConfigurePrimaryHttpMessageHandler here took a lot of time to find out. The Polly docs did not offer a good example of using IHttpClientFactory combined with throwing errors, which will be shown later in this article in the RandomHttpErrorHandler
DelegatingHandler. It is also important to configure the primary http message handler here before the resilience handler is added using AddResilienceHandler.
The following summary of resilience strategies shows that Polly offers more strategies than just the retry strategy. Every resilience strategy here can be combined to offer multiple resilience strategy to obtain the desired level of QoS = Quality of Service.
Strategy | Description | Can Be Combined? |
---|---|---|
Retry | Retries a failed operation based on rules (e.g., delay, max attempts). | ✅ Yes |
Circuit Breaker | Stops calls when failures exceed a threshold, allowing time to recover. | ✅ Yes |
Timeout | Limits how long an operation can run before it's considered failed. | ✅ Yes |
Bulkhead | Limits concurrent executions to prevent overload. | ✅ Yes |
Fallback | Provides an alternative result or action when the primary fails. | ✅ Yes |
Rate Limiter | Controls the rate of requests to avoid overwhelming a service. | ✅ Yes |
Adding support to test out random http request errors
To verify that the retry resilience strategy works, the following DelegatingHandler shown in the code in PollyExtensions will random throw HttpRequestException with a set estimated failure rate in percentage to be able to to end-to-end testing of retries. Debugging the endpoints to be defined later in this article shows the retries are working with this setup.RandomHttpErrorHandler.cs
namespace HttpClientUsingPolly
{
public class RandomHttpErrorHandler : DelegatingHandler
{
private readonly Random _random = new();
private readonly double _errorChance;
public RandomHttpErrorHandler(double errorChance)
{
_errorChance = errorChance;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (_random.NextDouble() < (_errorChance / 100))
{
// Pick a random transient status code
var httpErrorsPossible = new[]
{
System.Net.HttpStatusCode.RequestTimeout, // 408
System.Net.HttpStatusCode.InternalServerError, // 500
System.Net.HttpStatusCode.BadGateway, // 502
System.Net.HttpStatusCode.ServiceUnavailable, // 503
System.Net.HttpStatusCode.GatewayTimeout // 504
};
var chosenStatus = httpErrorsPossible[_random.Next(httpErrorsPossible.Length)];
throw new HttpRequestException($"Simulated Http error: {(int)chosenStatus}", null, chosenStatus);
}
return await base.SendAsync(request, cancellationToken);
}
}
}
PollyPipelineHelper.cs
The following code makes it easier to use the ResiliencePipelineProvider that will look up configured resilience pipelines that are defined in the method AddNamedPollyPipelines further up in the article.
using Polly.Registry;
namespace HttpClientUsingPolly
{
public static class PollyPipelineHelper
{
public static ValueTask<HttpResponseMessage> ExecuteWithPolicyAsync(
this ResiliencePipelineProvider<string> pipelineProvider,
string policyName,
Func<CancellationToken, Task<HttpResponseMessage>> action,
CancellationToken cancellationToken = default)
{
var pipeline = pipelineProvider.GetPipeline(policyName);
return pipeline.ExecuteAsync(
async ct => await action(ct),
cancellationToken);
}
}
}
Program.cs
The folllowing setup is done in Program.cs, using the extension methods defined further up in the article. The setup below is the setup for the demo code, which is an Asp.net Web Api application. Minimal Api endpoints will be defined for the demo.
namespace HttpClientUsingPolly
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
builder.Services.AddPollyHttpClient(); //set up polly v9 enabled http client (http client will be created via IHttpClientFactory)
builder.Services.AddNamedPollyPipelines(); // set up polly v9 enabled resilience pipepline (http client will be created via ResiliencePipelineProvider
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapGitHubUserEndpoints();
app.MapControllers();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.Run();
}
}
}
Finally, the following Minimal Api endpoint methods first uses the resilience provider. As we can see, there is quite a bit of setup to make use of Polly using a resilience pipeline.
Minimal Api Endpoints using Polly enabled resilience pipeline
The following Github endpoints uses Polly resilience pipeline defined further up in the article. Both the IHttpClientFactory and ResiliencePipelineProvider are injected from DI container using the [FromService] attribute. There is some juggling to get to the httpClient to perform the request.GithubEndpoints.cs
using Microsoft.AspNetCore.Mvc;
using Polly.Registry;
using System.Net.Http.Headers;
using System.Text.Json;
namespace HttpClientUsingPolly
{
public static class GithubEndpoints
{
public const string HttpClientName = "GitHubClient";
public static void MapGitHubUserEndpoints(this WebApplication app)
{
app.MapGet("/github-v1/{username}", async (
string username,
[FromServices] IHttpClientFactory httpClientFactory,
[FromServices] ResiliencePipelineProvider<string> resiliencePipelineProvider) =>
{
string url = $"https://api.github.com/users/{username}";
using var client = httpClientFactory.CreateClient(HttpClientName);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
var response = await resiliencePipelineProvider.ExecuteWithPolicyAsync(
PollyExtensions.RetryResiliencePolicy,
ct => client.GetAsync(url, ct));
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var user = JsonSerializer.Deserialize<JsonElement>(json);
return Results.Json(user);
});
app.MapGet("/test-retry-v1", async (
[FromServices] IHttpClientFactory httpClientFactory,
[FromServices] ResiliencePipelineProvider<string> resiliencePipelineProvider) =>
{
using var client = httpClientFactory.CreateClient(HttpClientName);
var response = await resiliencePipelineProvider.ExecuteWithPolicyAsync(
PollyExtensions.RetryResiliencePolicy,
ct => client.GetAsync("https://example.com", ct));
return Results.Json(response);
});
}
}
}
Simpler use of Polly via IHttpClientFactory
The following Minimal API endpoints shows a simpler usage of Polly, where the use of Polly retry resilience strategy are implicit. It was defined in the method AddPollyHttpClient further up in this article.
//use keyed httpclient and do not go via pipeline provider
app.MapGet("/github-v2/{username}", async (
string username,
[FromServices] IHttpClientFactory httpClientFactory) =>
{
string url = $"https://api.github.com/users/{username}";
using var client = httpClientFactory.CreateClient(HttpClientName);
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var user = JsonSerializer.Deserialize<JsonElement>(json);
return Results.Json(user);
});
app.MapGet("/test-retry-v2", async (
[FromServices] IHttpClientFactory httpClientFactory) =>
{
using var client = httpClientFactory.CreateClient(HttpClientName);
var response = await client.GetAsync("https://example.com");
return Results.Json(response);
});
As shown above, the Minimal API endpoints above uses IHttpClientFactory. The complexity of Polly is hidden and we can focus in the minimal API method to instantiate our HttpClient and make request and then retrieve the response content and return the result in the minimal API endpoint.
Further resources - Videos showing more information about Polly
Title | Description | Length | Link |
---|---|---|---|
Visual Studio Toolbox Live – Ensuring Resilience with Polly | Martin Costello walks through Polly strategies like Retry, Circuit Breaker, Timeout, and how to use them in .NET apps. | 59:29 | Watch |
Learn Live – Implement Resiliency in a Cloud-Native ASP.NET Core Microservice | Deep dive into code-based and infrastructure-based resiliency using Polly and service meshes like Linkerd. | 1:25:06 | Watch |
Implementing Resilience Strategies with Polly in ASP.NET Core | Step-by-step guide to applying retry, circuit breaker, and timeout policies in ASP.NET Core using Polly. | ~15 min | Watch |
Coding Short: Resilient .NET Apps with Polly | Shawn Wildermuth gives a concise overview of Polly with practical examples of retry and circuit breaker usage. | 14:07 | Watch |
Building Resilient Cloud Services with .NET 8 | .NET Conf 2023 | Explores .NET 8 resiliency features and Polly integration in cloud-native applications. | ~45 min | Watch |
No comments:
Post a Comment