M. Mis
M. Mis

Reputation: 127

How Setup Parameters for AddHttpClient when IHttpClientFactory is created?

I'm try setting up a IHttpClientFactory, and i'd like to know how to send it parameters when it is created, those parameters i need to assign to retry policy.

I'm using .Net Core 2.2 and Microsoft.Extensions.Http.Polly, I've read this post

I have this is Startup.cs

services.AddHttpClient("MyClient", c =>
{
    c.BaseAddress = new Uri("http://interface.net");
    c.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

I used it in this way

private readonly IHttpClientFactory _iHttpClientFactory;

public ValuesController(IHttpClientFactory iHttpClientFactory)
{
    _iHttpClientFactory = iHttpClientFactory;
}

public async Task<ActionResult<string>> Get()
{
    var client = _iHttpClientFactory.CreateClient("MyClient");
    var response = await client.GetAsync("/Service?Id=123");
    response.EnsureSuccessStatusCode();
    var result = await response.Content.ReadAsStringAsync();
    return result;
}

I'd like to know if there is a way to send parameters when i execute the CreateClient, for assign to retryCount and sleepDuration in the AddTransientHttpErrorPolicy, in this case 3 and 600 respectively, because i need to create clients with different retryCounts and sleepDurations and those values can change.

Something like this

var retryCount = 5;
var sleepDuration = 400;
var client = _iHttpClientFactory.CreateClient("MyClient", retryCount, sleepDuration);

Or another way?

Upvotes: 5

Views: 15075

Answers (3)

Johannes Mols
Johannes Mols

Reputation: 1041

I had a similar requirement where I had to retrieve the client lifetime from an API in the setup phase, which also returned an authentication token that I need in the setup method. I solved it with a custom extension method:

public static IHttpClientBuilder AddHttpClientWithCustomLifetime<TClient, TImplementation>(
    this IServiceCollection services, Func<(TimeSpan, string)> authenticationFunc, Func<string, Action<HttpClient>> configureClient)
    where TClient : class where TImplementation : class, TClient
{
    var (lifetime, authToken) = authenticationFunc();
    return services.AddHttpClient<TClient, TImplementation>(configureClient(authToken)).SetHandlerLifetime(lifetime);
}

This first calls some function that returns the lifetime and authentication token. The token is then passed into the actual config function, and the lifetime is used in the SetHandlerLifetime function.

I call it like this in Startup:

(TimeSpan, string) ConfigureLifetimeAsync(IntegrationConfiguration config)
{
    var result = AsyncHelper.RunSync(async () =>
    {
        using var authClient = new HttpClient { BaseAddress = new Uri(config.AuthUrl, UriKind.Absolute) };
        var parameters = new List<KeyValuePair<string, string>>
        {
            // ...
        };
        return await authClient.PostAsync<OAuthTokenResponse>(string.Empty, parameters, new SerilogLoggerFactory(Log.Logger).CreateLogger<ILogger<Integration>>());
    });
    return (TimeSpan.FromSeconds(int.Parse(result.Result.ExpiresIn)), result.Result.AccessToken);
}
services.AddHttpClientWithCustomLifetime<IIntegration, Integration>(() => ConfigureLifetimeAsync(config), authToken => c =>
{
    c.DefaultRequestHeaders.Add("Authorization", $"Bearer {authToken}");
    c.DefaultRequestHeaders.Add("X-Api-Key", config.ApiKey);
});

Upvotes: 0

Peter Csala
Peter Csala

Reputation: 22829

If you need to specify the retryCount and sleepDuration at the usage of HttpClient then you can do that through the HttpRequestMessage with the help of Polly's Context.

Define helpers

In order to easy the usage of Context (which is a Dictionary<string, object> under the hood) lets define some extension methods

public static class ContextExtensions
{
    private const string RetryCountKey = "RetryCount";
    private const string SleepDurationKey = "SleepDuration";

    public static Context WithRetryCount(this Context context, int retryCount)
    {
        context[RetryCountKey] = retryCount;
        return context;
    }

    public static Context WithSleepDuration(this Context context, TimeSpan sleepDuration)
    {
        context[SleepDurationKey] = sleepDuration;
        return context;
    }

    public static int? GetRetryCount(this Context context)
        => context[RetryCountKey] as int?;

    public static TimeSpan? GetSleepDuration(this Context context)
        => context[RetryCountKey] as TimeSpan?;
}

Register a named client

The AddTransientHttpErrorPolicy does not have an overload which allow us to access the HttpRequestMessage so, we have to switch back to AddPolicyHandler

services.AddHttpClient("MyClient", client =>
{
    client.BaseAddress = new Uri("http://httpstat.us/");
    //...
})
.AddPolicyHandler((_, request) =>
{
    var context = request.GetPolicyExecutionContext();
    return HttpPolicyExtensions.HandleTransientHttpError()
      .WaitAndRetryAsync(
        context.GetRetryCount() ?? 3,
        _ => context.GetSleepDuration() ?? TimeSpan.FromMilliseconds(300),
        onRetry: (_, __) => Console.WriteLine("Retry is triggered"));
});
  • We can retrieve the Context via the GetPolicyExecutionContext method of the HttpRequestMessage
  • Because the GetRetryCount and GetSleepDuration might return null that's why we can define here some fallback values
  • I've specified an onRetry just for debugging purposes

Provide parameters at usage

As you have already guessed it there is a SetPolicyExecutionContext method as well on the HttpRequestMessage

private readonly HttpClient client;
public XYZController(IHttpClientFactory factory)
{
    client = factory.CreateClient("MyClient");
}

[HttpGet]
public async Task<string> Get()
{
    var context = new Context()
         .WithRetryCount(5)
         .WithSleepDuration(TimeSpan.FromSeconds(1));
    var request = new HttpRequestMessage(HttpMethod.Get, "/408");
    request.SetPolicyExecutionContext(context);
    
    _ = await client.SendAsync(request);
    return "...";
}

Upvotes: 0

Chris Pratt
Chris Pratt

Reputation: 239430

As far as I am aware, you cannot. That's not really how IHttpClientFactory is designed to work. The idea is having reusable clients for specific scenarios, not an infinitely configurable client to be shared across different scenarios, and the Polly config pretty much goes along with that.

In other words, the design is that you'd configure a client or clients with the various retry policies and such you want, and then you'd specify which of those you want for a particular scenario.

services.AddHttpClient("MyClient", c =>
{
    c.BaseAddress = new Uri("http://interface.net");
    c.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

services.AddHttpClient("MyClient2", c =>
{
    c.BaseAddress = new Uri("http://interface.net");
    c.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(5, _ => TimeSpan.FromMilliseconds(400)));

Then, you could call CreateClient with either "MyClient" or "MyClient2". To keep from repeating yourself with the main client config, you could either factor out the body:

Action<HttpClient> myClientConfig = c =>
{
    ...
}

Then:

services.AddHttpClient("MyClient", myClientConfig);

Or, you might consider creating a custom extension:

public static IHttpClientBuilder AddMyClient(this IServiceCollection services, string clientName)
{
    return services.AddHttpClient(clientName, c =>
    {
        ...
    });
}

And then:

services.AddMyClient("MyClient")
    .AddTransientHttpErrorPolicy(...);

In general, though, the Polly policies should pretty much be bound to a particular use case. You'll know what the particular API/endpoint needs and you'll build a policy directly around that. A different API/endpoint may need different handling, but that's an argument for a different client in such a case.

Upvotes: 8

Related Questions