Extensions: Add a way to use Dependency injection in the polly policies

Created on 5 Nov 2018  路  10Comments  路  Source: dotnet/extensions

I would like to be able to inject my logger and maybe use a entity framework context to keep log of the retries, however, i can not find a way to do it, it would be easy if the context include the service provider.

Relates to #86, but in a broad scope, talking about dependency injection in general, not just logs

area-httpclientfactory question

All 10 comments

@antonioortizpola You can pass any information to the Polly policy execution via the execution-scoped Polly.Context. @stevejgordon has written up this blog example, passing an ILogger to the policy execution.

In that blog the retry policy is used outside the call through HttpClient, but the principle (passing data into the execution via Polly.Context) works just as well where the policy has been configured via HttpClientFactory's .AddPolicyHandler(...) approach, as an earlier blog from Steve shows.

The approach can of course be extended to pass any other item to the Polly.Context - an ILoggerFactory or (if desired) IServiceProvider.

@reisenberger Thanks for the answer, i could not find that article in my previous search, i guess i need to improve my google skills.

Before checking your links, I tried putting everything in my Typed HttpClient, something like this:

public class MyServiceHttpClient
{
    private static readonly Random Jitterer = new Random();
    private readonly HttpClient _client;
    private readonly ILogger<MyServiceHttpClient> _logger;

    public MyServiceHttpClient(
        HttpClient httpClient,
        IOptions<TelcelConfig> configuration,
        ILogger<MyServiceHttpClient> logger)
    {
        httpClient.BaseAddress = new Uri(configuration.Value.Endpoint);
        _client = httpClient;
        _logger = logger;
    }

    public async Task<ResponseWithRequestLog<string>> CallService(string param)
    {
        var response = await Policy
            .Handle<Exception>()
            .WaitAndRetryAsync(3, retryAttempt =>
            {
                _logger.LogWarning("{retry} retry my service request for param {msisdn}", retryAttempt, param);
                return TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
                       + TimeSpan.FromMilliseconds(Jitterer.Next(0, 100));
            })
            .ExecuteAsync(() => _client.GetAsync(param));

        response.EnsureSuccessStatusCode();
        var result = await response.Content
            .ReadAsJsonAsync<ResponseWithRequestLog<string>>();
        return result;
    }
}

I know it is not that reusable, but i have direct access to my injected properties, i tested it and it seems to work fine, should I be worry about something? or this solution should also works?

@antonioortizpola Your sample code looks like it will work fine for the Typed client, given the usage the MyServiceHttpClient class makes of the HttpClient instance injected by HttpClientFactory.

The general case for Polly retry policies with HttpClient does not use the retry policy outside the call to _client.SomeOverloadAsync(...), because for overloads where the caller supplies the HttpRequestMessage a retry used outside the _client.SendAsync(...) call will fail on the check for HttpRequestMessage being reused. However, the single overload on HttpClient which you use within MyServiceHttpClient is benign for (is ok with) this retry style, because that overload chains to create a new HttpRequestMessage instance per try.

@antonioortizpola There is also an .AddPolicyHandler(...) overload providing an IServiceProvider input parameter.

So you can also configure a typed client, then use that overload to configure a policy which resolves the ILogger<FooTypedClient> needed:

services.AddHttpClient<MyServiceHttpClient>(client =>
{
    /* configuration */
})
.AddPolicyHandler((serviceProvider, request) => 
    HttpPolicyExtensions.HandleTransientHttpError()
        .WaitAndRetryAsync(3, 
            sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), 
            onRetry: (outcome, timespan, retryAttempt, context) =>
            {
                serviceProvider.GetService<ILogger<MyServiceHttpClient>>()
                    .LogWarning("Delaying for {delay}ms, then making retry {retry}.", timespan.TotalMilliseconds, retryAttempt);
            }
            ));

EDITED: Moved code example away from original sample, to demonstrate a preferred Polly pattern which separates calculating wait-before-retry (sleepDurationProvider) from logging (onRetry).

Great, i did not saw that method, thanks a lot for the help, i will try with that overload.

I would like to see this in the documentation, specially the last part with the ServiceProvider, since all the examples were so simple, this could point the way to other users like me.

Thanks for the quick response, my questions have been answered so this can be closed.

@rynowak I believe this above provides a better answer for how to use services from DI in the Polly policies configured by HttpClientFactory.

Do you think there is anything more we need to add around this, either in code or documentation?

From memory we added this overload at the time to support PolicyRegistry, but it also serves this DI use case. I noticed this hasn't been widely picked up in documentation/blogs, so I added this to the Polly and HttpClientFactory documentation maintained by the Polly team.

Just as a comment, for me looks great, from there the user can expand to whatever use with DI they want, like registering the retries in a service or a simple log as the example shows.

QQ - the example, should that maintain the same instance of the policy between multiple HttpClient's? Because in my tests it doesn't seem to be and a new policy is being instanciated for each run albeit with the right service provider being injected.

That causes an issue with CircuitBreaker in particular because its state isn't then shared between multiple clients.

Is that something I'm doing incorrectly, expected behaviour or a real issue?

Sorry for the delay, it's been a little bit chaotic with the issue/repo move here.

I'd also suggest in this case resolving the logger a single time and then closing over it rather than closing over the service provider.

@kieranbenton - yes, there are definitely cases where you want to share a policy between multiple clients. Are you saying that this is too hard to get right? or that you want to see an example of that?

@scottaddie - I think this is also something we want to add to the docs. The scenario here is when you need access to some DI services inside of a policy.

Closing this since docs issues are tracked elsewhere.

Was this page helpful?
0 / 5 - 0 ratings