Reputation: 127
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
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
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
.
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?;
}
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"));
});
Context
via the GetPolicyExecutionContext
method of the HttpRequestMessage
GetRetryCount
and GetSleepDuration
might return null
that's why we can define here some fallback valuesonRetry
just for debugging purposesAs 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
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