Skip to content

Polly and HttpClientFactory

martincostello edited this page Sep 28, 2023 · 66 revisions

Using Polly with HttpClient factory from ASPNET Core 2.1 onwards

ℹ️ This documentation describes the previous Polly v7 API. If you are using the new v8 API, please refer to pollydocs.org.

TL;DR HttpClient factory in ASPNET Core 2.1 provides a way to pre-configure instances of HttpClient which apply Polly policies to every outgoing call (among other benefits).

Sidenote: If you experience diamond dependency conflicts using Polly v7 with HttpClientFactory, follow the resolution here.

What is HttpClient factory?

From ASPNET Core 2.1, Polly integrates with IHttpClientFactory. HttpClient factory is a factory that simplifies the management and usage of HttpClient in four ways. It:

  • allows you to name and configure logical HttpClients. For instance, you may configure a client that is pre-configured to access the github API;

  • manages the lifetime of HttpClientMessageHandlers to avoid some of the pitfalls associated with managing HttpClient yourself (the disposing-it-too-often-can-cause-socket-exhaustion but also only-using-a-singleton-can-miss-DNS-updates aspects);

  • provides configurable logging (via ILogger) for all requests and responses performed by clients created with the factory;

  • provides a simple API for adding middleware to outgoing calls, be that for logging, authorization, service discovery, or resilience with Polly.

The Microsoft early announcement speaks more to these topics, and Steve Gordon's quartet of blog posts (1; 2; 3; 4) are also an excellent read for deeper background and some great worked examples. UPDATE: The official documentation is also now out.

Using Polly with IHttpClientFactory

Step 1 Reference the ASPNET Core 2.1 (or later) packages and Microsoft.Extensions.Http.Polly

Have your project grab the ASPNET Core 2.1 packages from nuget. You'll typically need the AspNetCore metapackage, and the extension package Microsoft.Extensions.Http.Polly.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0" />
    <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="2.1.0" />
  </ItemGroup>

</Project>

Note: later versions of these packages may be available when you read this.

Step 2 Configure a client with Polly policies, in Startup

Define a named HttpClient configuration

In your standard Startup.ConfigureServices(...) method, start by configuring a named client as below:

public void ConfigureServices(IServiceCollection services)
{
    // Configure a client named as "GitHub", with various default properties.
    services.AddHttpClient("GitHub", client =>
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    });

    // ...
}

(We've used magic strings for clarity, but of course you can obtain those from config or declare them once as consts.)

We'll focus on configuring this with Polly policies, but there are many more options for configuring the named HttpClient which you can read about from the official docs, or Steve Gordon or Scott Hanselman. To keep the examples in this post shorter, we've used named clients, but the documentation and blogs above also cover how to use typed clients, which offer the advantages of strong-typing and allow you to build overloads on the typed-client focused on your specific needs.

Fluently extend that client configuration with Polly policies

To apply Polly policies, you simply extend the above example with some fluent configuration:

services.AddHttpClient("GitHub", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
})
.AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
{
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(5),
    TimeSpan.FromSeconds(10)
}));

This example creates a policy which will handle typical transient faults, retrying the underlying http request up to 3 times if necessary. The policy will apply a delay of 1 second before the first retry; 5 seconds before a second retry; and 10 seconds before the third.

The overload .AddTransientHttpErrorPolicy(...) is one of a number of options, which we'll look at after covering the basics.

Step 3 Consume the configured HttpClient

For completeness, here's an example of consuming the configured HttpClient. For a named client (as the above example), take an IHttpClientFactory by dependency injection at the usage site. Then use that factory to obtain an HttpClient configured to the specification you defined in Startup:

public class MyController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;

    public MyController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public Task<IActionResult> SomeAction()
    {
        // Get an HttpClient configured to the specification you defined in StartUp.
        var client = _httpClientFactory.CreateClient("GitHub");

        return Ok(await client.GetStringAsync("/someapi"));
    }
}

The call await client.GetStringAsync("/someapi") applies the configured policies within the call, as described in the next section.

Again, Steve Gordon's and Scott Hanselman's blogs give richer examples, including if you prefer typed clients.

How are the Polly policies applied?

The policy or policies configured on your HttpClient are applied to outbound calls by Polly-based DelegatingHandlers.

This means the policies will be applied to all outgoing calls through that configured HttpClient.

If you've tried in the past to hand-craft retries outside calls to HttpClient.SendAsync(...) which pass in an HttpRequestMessage, you may have discovered that the HttpRequestMessage passed in cannot be reused once sent (doing so raises an InvalidOperationException). The DelegatingHandler approach avoids this problem.

A DelegatingHandler is simply middleware for an outbound http call: see Steve Gordon's third blog post for a great introduction to how delegating handlers work.

Configuring the Polly policies

Using .AddTransientHttpErrorPolicy(...)

Let's look at the example from Step 2 above again:

services.AddHttpClient("GitHub", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
})
.AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
{
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(5),
    TimeSpan.FromSeconds(10)
}));

This uses a new convenience method, .AddTransientHttpErrorPolicy(...). This configures a policy to handle errors typical of Http calls:

  • Network failures (System.Net.Http.HttpRequestException)
  • HTTP 5XX status codes (server errors)
  • HTTP 408 status code (request timeout)

Using .AddTransientHttpErrorPolicy(...) pre-configures what the policy handles. The builder => builder clause then specifies how the policy will handle those faults.

In the builder => builder clause you can choose any reactive policy from Polly's offerings: a retry strategy (as in the above example), circuit-breaker or fallback policy.

The choice in .AddTransientHttpErrorPolicy(...) to handle HttpRequestException, HTTP 5xx, HTTP 408 is a convenience option, but not mandatory. If that error filter doesn't suit your needs - which you should think through - you can extend the definition of errors to handle, or build an entirely bespoke Polly policy.

Using any policy configured via the traditional Polly syntax

Overloads are also available taking any IAsyncPolicy<HttpResponseMessage>, so you can define and apply any kind of policy: you specify both the what to handle and how to handle.

This example demonstrates .AddPolicyHandler(...) to add a policy where we coded our own specification of faults to handle:

var retryPolicy = Policy.Handle<HttpRequestException>()
    .OrResult<HttpResponseMessage>(response => MyCustomResponsePredicate(response))
    .WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    }));

services.AddHttpClient(/* etc */)
    .AddPolicyHandler(retryPolicy);

As well as Polly's reactive policies (such as retry and circuit-breaker), these overloads mean you can also use proactive policies such as timeout:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);

services.AddHttpClient(/* etc */)
    .AddPolicyHandler(timeoutPolicy);

All calls through HttpClient return an HttpResponseMessage, so the policies configured must be of type IAsyncPolicy<HttpResponseMessage>. Non-generic policies IAsyncPolicy can also be converted to IAsyncPolicy<HttpResponseMessage> with a simple convenience method:

var timeoutPolicy = Policy.TimeoutAsync(10);

services.AddHttpClient(/* etc */)
    .AddPolicyHandler(timeoutPolicy.AsAsyncPolicy<HttpResponseMessage>());

Extending the convenience .AddTransientHttpErrorPolicy(...) definition

The definition of errors handled by .AddTransientHttpErrorPolicy(...) is also available from a Polly extension package, Polly.Extensions.Http (github; nuget).

Using this allows you to take the base specification of errors to handle (HttpRequestException, HTTP 5xx, HTTP 408) and extend it. For example, the policy configured below would handle status code 429 additionally:

using Polly.Extensions.Http; // After installing the nuget package: Polly.Extensions.Http

// ..

var policy = HttpPolicyExtensions
  .HandleTransientHttpError() // HttpRequestException, 5XX and 408
  .OrResult(response => (int)response.StatusCode == 429) // RetryAfter
  .WaitAndRetryAsync(/* etc */);

Applying multiple policies

All overloads for configuring policies can also be chained to apply multiple policies:

services.AddHttpClient(/* etc */)
    .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    }))
    .AddTransientHttpErrorPolicy(builder => builder.CircuitBreakerAsync(
        handledEventsAllowedBeforeBreaking: 3,
        durationOfBreak: TimeSpan.FromSeconds(30)
    ));

What order are multiple policies applied in?

When you configure multiple policies (as in the above example), the policies are applied to each call from outer (first-configured) to inner (last-configured) order.

In the above example, the call will:

  1. first be placed through the (outer) retry policy, which will in turn:
  2. place the call through the (inner) circuit-breaker, which in turn:
  3. makes the underlying http call.

multiplepolicyhttpmessagehandlers

The sequencing of policies in this example was chosen because the circuit-breaker may change state in one of those periods (1, 5 or 10 seconds) when the retry policy is waiting between tries. The circuit-breaker is configured 'inside' the retry, so that the circuit state is tested again as part of the action of making a retry.

The above example applies two policies (retry and circuit-breaker), but any number is possible. A common useful combination might be to apply a retry, a circuit-breaker, and a timeout-per-try (see below).

How does this compare to PolicyWrap?

For those familiar with Polly's PolicyWrap, configuring multiple policies with the pattern shown above is entirely equivalent to using a PolicyWrap. All the usage recommendations in the PolicyWrap wiki apply.

Combining PolicyHttpMessageHandler with other DelegatingHandlers

Likewise, if you combine PolicyHttpMessageHandler with other DelegatingHandlers, consider whether the policy handlers should be 'inside' or 'outside' the other delegating handlers in the middleware pipeline you construct. The sequence in which DelegatingHandlers are applied corresponds to the sequence you configure them in after the .AddHttpClient(/* etc */) call.

Selecting policies dynamically

Overloads of .AddPolicyHandler(...) exist allowing you to select policies dynamically based on the request.

One use case for this is to apply different policy behavior for endpoints which are not idempotent. POST operations typically are not idempotent. PUT operations should be idempotent, but may not be for a given API (there is no substitute for knowing the behavior of the API you are calling). So, you might want to define a strategy which retries for GET requests but not for other http verbs:

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    });
var noOpPolicy = Policy.NoOpAsync().AsAsyncPolicy<HttpResponseMessage>();

services.AddHttpClient(/* etc */)
    // Select a policy based on the request: retry for Get requests, noOp for other http verbs.
    .AddPolicyHandler(request => request.Method == HttpMethod.Get ? retryPolicy : noOpPolicy);

The above example uses NoOp policy for http verbs other than GET. NoOp policy simply executes the underlying call 'as is', without any additional policy behavior.

With stateful policies, use as selector, not factory

When using the .AddPolicyHandler(policySelector: request => ...) (and similar) overloads on HttpClientClientFactory with stateful policies such as circuit-breaker and bulkhead, you must make sure that the policySelector does not manufacture a new instance per request, but instead selects a single instance of the circuit-breaker or bulkhead. This is so that the single instance can be statefully reused across requests. Do not code:

// BAD CODE (do not use)
services.AddHttpClient(/* etc */)
   .AddPolicyHandler(request => /* some func sometimes returning */
       HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(...)) // This will manufacture a new circuit-breaker per request.

Instead:

var circuitBreaker = HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(...);
services.AddHttpClient(/* etc */)
    .AddPolicyHandler(request => /* some func sometimes returning */ circuitBreaker )

Note that there is one more advanced case. If you are calling a dynamic set of downstream nodes guarded by circuit-breakers, you cannot pre-define (at DI configuration time) a circuit-breaker per downstream node; instead, you need to create a new circuit-breaker the first time a new node is used (policy factory approach), but then each subsequent time that node is used, select the same previously-generated circuit-breaker (policy selector approach). For discussion of this case see the heading Using a GetOrAdd(...)-style approach on PolicyRegistry below.

Selecting policies from a PolicyRegistry

Polly also provides PolicyRegistry as a central store for policies you might reuse in multiple places in your application. Overloads of .AddPolicyHandler(...) exist allowing you to select a policy from the registry.

The following example:

  • creates a PolicyRegistry and adds some policies to it using collection-initialization syntax
  • registers that PolicyRegistry with the IServiceCollection,
  • defines a logical HttpClient configuration using different policies from the registry.

Code:

var registry = new PolicyRegistry()
{
    { "defaultretrystrategy", HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(/* etc */) },
    { "defaultcircuitbreaker", HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(/* etc */) },
};

services.AddPolicyRegistry(registry);

services.AddHttpClient(/* etc */)
    .AddPolicyHandlerFromRegistry("defaultretrystrategy")
    .AddPolicyHandlerFromRegistry("defaultcircuitbreaker");

More complex use cases for PolicyRegistry include dynamically updating the policies in your registry from an external source, to facilitate dynamic reconfiguration of policies during running.

There are also more complex overloads on HttpClientFactory which allow selecting policies from registry dynamically, based on the HttpRequestMessage.

Using a GetOrAdd(...)-style approach on PolicyRegistry

A more advanced case is where you may want to .GetOrAdd(...) policies in PolicyRegistry. If you are calling a dynamic set of downstream nodes guarded by circuit-breakers, you cannot pre-define (at DI configuration time) a circuit-breaker per downstream node; instead, you need to create a new circuit-breaker the first time a new node is used (policy factory approach), but then each subsequent time that node is used, select the same previously-generated circuit-breaker (policy selector approach). For overloads supporting this scenario, see this discussion. This demonstrates an overload on IHttpClientFactory allowing you do store-and-retrieve policies (for example circuit-breakers) in PolicyRegistry using a GetOrAdd(...)-style approach.

Use Case: Applying timeouts

HttpClient already has a Timeout property, but how does this apply when a retry policy is in use? And where does Polly's TimeoutPolicy fit?

  • HttpClient.Timeout will apply as an overall timeout to each entire call through HttpClient, including all tries and waits between retries.
  • To apply a timeout-per-try, configure a RetryPolicy before a Polly TimeoutPolicy.

In this case, you may want the retry policy to retry if any individual try timed out. To do this, make the retry policy handle the TimeoutRejectedException which Polly's timeout policy throws.

This example uses the Polly.Extensions.Http package described earlier, to extend the convenience error set (HttpRequestException, HTTP 5XX, and HTTP 408) with extra handling:

using Polly.Extensions.Http;

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .Or<TimeoutRejectedException>() // thrown by Polly's TimeoutPolicy if the inner call times out
    .WaitAndRetryAsync(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        });

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10); // Timeout for an individual try

serviceCollection.AddHttpClient("GitHub", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    client.Timeout = TimeSpan.FromSeconds(60); // Overall timeout across all tries
})
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler(timeoutPolicy); // We place the timeoutPolicy inside the retryPolicy, to make it time out each try.

Always use TimeoutPolicy's optimistic timeout with HttpClient (more info). (This is the default, so does not need to be explicitly stated - per the code sample above.)

If you configure TimeoutPolicy outside RetryPolicy

If you have configured the retry and timeout policy in the other order (configuring timeoutPolicy before, thus outside, the retryPolicy), that TimeoutPolicy will instead act as an overall timeout for the whole operation (just as HttpClient.Timeout does), not as a timeout-per-try. This is a natural consequence of the way multiple policies act as nested steps in a middleware pipeline.

Use Case: scoping CircuitBreakers and Bulkheads

Policy instances applied to a named HttpClient configuration are shared across all calls through that HttpClient configuration.

For the stateful policy circuit breaker, this means that all calls through a named HttpClient configured with a circuit-breaker will share that same circuit state.

This usually plays well with HttpClients configured via HttpClient factory, because those HttpClients typically define a common BaseAddress, meaning all calls are to some endpoint on that same BaseAddress. In that case, we might expect that if one endpoint on BaseAddress is unavailable, others will be too. The scoping then plays well: if calls to one endpoint through that HttpClient configuration break the circuit, the circuit will also be broken for others.

If, however, this 'shared' scoping of the circuit-breaker is not appropriate for your scenario, define separate named HttpClient instances and configure each with a separate circuit-breaker policy instance.

The same consideration applies if you use Polly's other stateful policy, Bulkhead. With a Bulkhead policy applied to a named HttpClient configuration, the Bulkhead capacity will be shared across all calls placed through that HttpClient.

Use Case: CachePolicy

Polly CachePolicy can be used in a DelegatingHandler configured via IHttpClientFactory. Polly is generic (not tied to Http requests), so at time of writing, the Polly CachePolicy determines the cache key to use from the Polly.Context. This can be set on an HttpRequestMessage request immediately prior to placing the call through HttpClient, by using an extension method: (add using Polly; to access the extension method)

request.SetPolicyExecutionContext(new Polly.Context("CacheKeyToUseWithThisRequest"));

Using CachePolicy with HttpClientFactory thus also requires that you use overloads on HttpClient which take an HttpRequestMessage as an input parameter.

Some additional considerations flow from the fact that caching with Polly CachePolicy in a DelegatingHandler caches at the HttpResponseMessage level.

Is caching at the HttpResponseMessage level the right fit?

If the HttpResponseMessage is the end content you wish to re-use (perhaps to re-serve in whole or in part), then caching at the HttpResponseMessage level may be a good fit.

In cases such as calling to a web service to obtain some serialized data which will then be deserialized to some local types in your app, HttpResponseMessage may not be the optimal granularity for caching.

In these cases, caching at the HttpResponseMessage level implies subsequent cache hits repeat the stream-read and deserialize-content operations, which is unnecessary from a performance perspective.

It may be more appropriate to cache at a level higher-up - for example, cache the results of stream-reading and deserializing to the local types in your app.

Considerations when caching HttpResponseMessage

  • The HttpResponseMessage can contain HttpContent which behaves like a forward-only stream - you can only read it once. This can mean that when CachePolicy retrieves it from cache the second time, the stream cannot be re-read unless you also reinitialize the stream pointer.

  • Consider de-personalisation and timestamping. Personal information (if any) and timestamps from a cached result may not be appropriate to re-supply to later requesters.

  • Exercise care to only cache 200 OK responses. Consider using code such as response.EnsureSuccessStatusCode(); to ensure that only successful responses pass to the cache policy. Or you can use a custom ITtlStrategy as described here.

Use Case: Exchanging information between policy execution and calling code

An execution-scoped instance of the class Polly.Context travels with every execution through a Polly policy. The role of this class is to provide context and to allow the exchange of information between the pre-execution, mid-execution, and post-execution phases.

For executions through HttpClients configured with Polly via HttpClientFactory, you can use the extension method HttpRequestMessage.SetPolicyExecutionContext(context), prior to execution, to set the Polly.Context that will be used with the Http call. Context has dictionary-semantics, allowing you to pass any arbitrary data.

var context = new Polly.Context();
context["MyCustomData"] = foo;

HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.SetPolicyExecutionContext(context);

var response = await client.SendAsync(request, cancellationToken);
// (where client is an HttpClient instance obtained from HttpClientFactory)

Polly passes that Context instance as an input parameter to any delegate hooks such as onRetry configured on the policy. For example, the HttpClient may have been pre-configured with a policy:

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    },
    onRetryAsync: async (outcome, timespan, retryCount, context) => {
        /* Do something with context["MyCustomData"] */
        // ...
    });

Delegate hooks may also set information on Context:

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    },
    onRetryAsync: async (outcome, timespan, retryCount, context) => {
        context["RetriesInvoked"] = retryCount;
        // ...
    });

services.AddHttpClient("MyResiliencePolicy", /* etc */)
    .AddPolicyHandler(retryPolicy);

And this information can be read from the context after execution:

var response = await client.SendAsync(request, cancellationToken);

var context = response.RequestMessage?.GetPolicyExecutionContext(); // (if not already held in a local variable)
if (context?.TryGetValue("RetriesInvoked", out int? retriesNeeded) ?? false)
{
    // Do something with int? retriesNeeded
}

Note that the context from HttpRequestMessage.GetPolicyExecutionContext() is only available post-execution if you used HttpRequestMessage.SetPolicyExecutionContext(Context) to set a context prior to execution.

Configuring policies to use services registered with DI, such as ILogger<T>

You may want to configure a policy which makes use of other services registered for Dependency Injection. A typical example would be to configure a policy whose callback delegates require an ILogger<T> resolved by dependency-injection.

An .AddPolicyHandler(...) overload exists allowing you to configure a policy which can resolve services from IServiceProvider when the policy is created.

Because the typical .NET Core logging pattern prefers generic ILogger<T>, this approach plays well with typed clients.

services.AddHttpClient<MyServiceHttpClient>(/* etc */)
    .AddPolicyHandler((services, request) => HttpPolicyExtensions.HandleTransientHttpError()
        .WaitAndRetryAsync(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        },
        onRetry: (outcome, timespan, retryAttempt, context) =>
        {
            services.GetService<ILogger<MyServiceHttpClient>>()?
                .LogWarning("Delaying for {delay}ms, then making retry {retry}.", timespan.TotalMilliseconds, retryAttempt);
        }
        ));

Note that the policy here obtains ILogger<MyServiceHttpClient>:

services.GetService<ILogger<MyServiceHttpClient>>() /* etc */

which means the logging will be categorized with MyServiceHttpClient. If you want the logging to be categorized with the class consuming the policy, and if multiple classes might consume the policy (meaning you do not know T for ILogger<T> at configuration time), then you can instead use the approach in the section below: pass an ILogger<T> to the policy at runtime using Polly.Context.

If you experience problems with logging being visible with IHttpClientFactory and Azure Functions, check out this discussion and this sample (September 2019; issue may resolve once azure-functions-host/issues/4345 closes).

Configuring HttpClientFactory policies to use an ILogger<T> from the call site

The technique in the section above resolves an ILogger<SomeConcreteType> from IServiceProvider at execution time, but the actual category SomeConcreteType of ILogger<SomeConcreteType> is defined at configuration time, and the technique relies on dynamic creation of policies.

There are two use cases which that approach does not suit:

  • You want to store policies in PolicyRegistry (as shown earlier in this documentation). These policies are typically created once only during StartUp, not dynamically, so do not fit the dynamic technique of creating a new policy which resolves a new ILogger<T> at execution time.

  • You want the category T of the logger ILogger<T> to be resolved at the call site, not at configuration time. For example, if the class consuming the HttpClient configured with the policy is MyFooApi, then MyFooApi might receive an ILogger<MyFooApi> in its constructor by dependency injection, and you might want to use that ILogger<MyFooApi> for the logging done by the policy's onRetry delegate.

Both cases can be solved by passing the ILogger<T> to the policy at the point of execution, using the execution-scoped Polly.Context. The general approach of using Context to pass information to a policy at execution time is described in an earlier section of this documentation.

To pass an ILogger<T> to a Polly policy via Polly.Context, we will first define some helper methods on Polly.Context:

public static class PollyContextExtensions
{
    private static readonly string LoggerKey = "ILogger";

    public static Context WithLogger<T>(this Context context, ILogger logger)
    {
        context[LoggerKey] = logger;
        return context;
    }

    public static ILogger GetLogger(this Context context)
    {
        if (context.TryGetValue(LoggerKey, out object logger))
        {
            return logger as ILogger;
        }

        return null;
    }

Note that these methods use the base interface Microsoft.Extensions.Logging.Logger because the policy's onRetry or onRetryAsync delegate is not generic and will not know the generic type T. But the actual instance passed at runtime will still be an ILogger<T>.

Configuring the policy in your StartUp class might then look something like this:

var registry = new PolicyRegistry()
{
    {
        "MyRetryPolicyResolvingILoggerAtRuntime",
        HttpPolicyExtensions
            .HandleTransientHttpError()
            .WaitAndRetryAsync(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        },
        onRetry: (outcome, timespan, retryAttempt, context) =>
        {
            context.GetLogger()?.LogWarning("Delaying for {delay}ms, then making retry {retry}.", timespan.TotalMilliseconds, retryAttempt);
        })
    },
};

services.AddPolicyRegistry(registry);

services.AddHttpClient("MyFooApiClient", /* etc */)
    .AddPolicyHandlerFromRegistry("MyRetryPolicyResolvingILoggerAtRuntime");

The example above has stored the policies in a PolicyRegistry and asked HttpClientFactory to retrieve them from the policy registry, but you can also use this technique with the HttpClientFactory overloads which do not involve a policy registry.

Finally, at the call site, where you execute through the HttpClient, you set the ILogger<T> on the Polly.Context before executing. An example class consuming the above policy might look something like this:

public class MyFooApi
{
    private readonly IHttpClientFactory httpClientFactory;
    private readonly ILogger<MyFooApi> logger;

    public MyFooApi(IHttpClientFactory httpClientFactory, ILogger<MyFooApi> logger)
    {
        this.logger = logger;
        this.httpClientFactory = httpClientFactory;

        // If MyFooApi is configured with Transient or Scoped lifetime,
        // you could alternatively call
        //     client = _httpClientFactory.CreateClient("MyFooApiClient")
        // here, and store private readonly HttpClient client, rather than IHttpClientFactory
    }

    public Task<SomeReturnType> SomeAction(...)
    {
        // (definition of SomeRequestUri and SomeCancellationToken omitted for brevity)

        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, SomeRequestUri);

        var context = new Polly.Context().WithLogger(logger);
        request.SetPolicyExecutionContext(context);

        var client = httpClientFactory.CreateClient("MyFooApiClient");

        var response = await client.SendAsync(request, SomeCancellationToken);

        // check for success, process the response and return it as SomeReturnType
    }
}

With this technique, you have to use one of the HttpClient.SendAsync(...) overloads taking an HttpRequestMessage parameter, as shown above.

The above examples use a named HttpClient configuration, but the same pattern can also be used with typed-clients on HttpClientFactory.

Clone this wiki locally