Polly and HttpClientFactory - App-vNext/Polly GitHub Wiki
âšī¸ 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.
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
HttpClient
s. For instance, you may configure a client that is pre-configured to access the github API; -
manages the lifetime of
HttpClientMessageHandler
s to avoid some of the pitfalls associated with managingHttpClient
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.
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.
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.
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.
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.
The policy or policies configured on your HttpClient
are applied to outbound calls by Polly-based DelegatingHandler
s.
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.
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.
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>());
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 */);
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)
));
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:
- first be placed through the (outer) retry policy, which will in turn:
- place the call through the (inner) circuit-breaker, which in turn:
- makes the underlying http call.
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).
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.
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.
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.
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.
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 theIServiceCollection
, - 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
.
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.
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 throughHttpClient
, including all tries and waits between retries. - To apply a timeout-per-try, configure a
RetryPolicy
before a PollyTimeoutPolicy
.
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 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.
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 HttpClient
s configured via HttpClient factory, because those HttpClient
s 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
.
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.
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.
-
The
HttpResponseMessage
can containHttpContent
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 customITtlStrategy
as described here.
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 HttpClient
s 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.
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).
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 duringStartUp
, not dynamically, so do not fit the dynamic technique of creating a new policy which resolves a newILogger<T>
at execution time. -
You want the category
T
of the loggerILogger<T>
to be resolved at the call site, not at configuration time. For example, if the class consuming theHttpClient
configured with the policy isMyFooApi
, thenMyFooApi
might receive anILogger<MyFooApi>
in its constructor by dependency injection, and you might want to use thatILogger<MyFooApi>
for the logging done by the policy'sonRetry
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
.