Per https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory#extending-the-convenience-addtransienthttperrorpolicy-definition I can see policies are added with a name.
var httpClientOptions = HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
.WaitAndRetryAsync(6,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
services.AddHttpClient(/*have to add a name*/)
.AddPolicyHandler(httpClientOptions);
I am using Simple Injector in parallel to the .net dependency injection, and I inject the http client factory into my consuming classes as below:
public PolygonIoApiFetcher(IHttpClientFactory clientFactory)
{
this.client = clientFactory.CreateClient(/* what to use here */);
}
My issue is that I have a number of functions written in other libraries that won't necessarily have the client factory policy name shared between libaries.
Furthermore, to keep things simple I would probably want a single poly retry policy for all my HttpClient
created by the factory - is there any way for IHttpClientFactory
to return a default HttpClient
with the default poly policy applied?
I do not want to use a typed registration either per https://github.com/simpleinjector/SimpleInjector/issues/654, and I need to control the lifetime of the object via the IHttpClientFactory
injected as opposed to the HttpClient
being injected into the consuming class.
In summary, the wiring/injection at startup should determine the policy injected and easily rewired/changed, and not by the consuming class.
I have provided what I have done so far, but pointers appreciated if this is, as i suspect, an already solved problem.
Idea below - create a wrapper class
The parameterless HttpClient CreateClient(this IHttpClientFactory factory)
extension method from System.Net.Http
cannot be overridden, so I thought to use the wrapper class and manually select the policy during injection:
public interface IPollyHttpClientFactory
{
HttpClient CreateClient();
}
public class PollyHttpClientFactoryWrapper
{
private readonly string policyName;
private readonly Container container;
public PollyHttpClientFactoryWrapper(
string policyName, Container container)
{
this.policyName = policyName ?? throw new ArgumentNullException();
this.container = container ?? throw new ArgumentNullException();
}
public HttpClient CreateClient()
{
return this.container
.GetInstance<IHttpClientFactory>()
.CreateClient(this.policyName);
}
}
And use during registration:
Container.RegisterSingleton<IHistoricalAggregates>(
() => new PolygonIoApiFetcher(
new PollyHttpClientFactoryWrapper("RetryPolicy", Container),
Container.GetInstance<ApiKeys>().PolygonIoApiKey));
The only "issue" is my classes would need to use the IPollyHttpClientFactory
interface, but I can live with that.
I'm not sure I can give you a satisfying answer, but here's an idea for a possible implementation. You can make the IPollyHttpClientFactory
conditional, in a way that each consumer gets its own version. This can be done using the RegisterConditional
method. For instance:
var container = new Container();
container.RegisterSingleton<IHistoricalAggregates, PolygonIoApiFetcher>();
container.RegisterConditional(
typeof(IPollyHttpClientFactory),
c => typeof(PollyHttpClientFactory<>)
.MakeGenericType(c.Consumer.ImplementationType),
Lifestyle.Singleton,
c => true);
container.MapPolicy<PolygonIoApiFetcher>("RetryPolicy");
Here, MapPolicy
is a custom extension method:
public static class ContainerPolicyExtensions
{
public static void MapPolicy<TImplementation>(
this Container container, string policyName) =>
container.RegisterInstance(new Policy<TImplementation>(policyName));
}
Policy<>
is a simple generic data object that allows holding the name of the policy to use for the HttpClient
(you can add any additional data relevant to creating http clients to this policy class):
public sealed record Policy<TConsumer>(string PolicyName);
PollyHttpClientFactory<>
is the generic implementation of IPollyHttpClientFactory
. It gets injected with the relevant Policy<T>
and it is, therefore, able to create a http client specific to its consumer:
public record PollyHttpClientFactory<TConsumer>(
IHttpClientFactory Factory, Policy<TConsumer> Policy)
: IPollyHttpClientFactory
{
public HttpClient CreateClient() =>
this.Factory.CreateClient(Policy.PolicyName);
}
Optionally, you could combine the Register
and MapPolicy
methods into a single extension method:
public static void RegisterWithPolicy<TService, TImplementation>(
this Container container, string policyName, Lifestyle lifestyle = null)
where TService : class
where TImplementation : class, TService
{
if (lifestyle is null) container.Register<TService, TImplementation>();
else container.Register<TService, TImplementation>(lifestyle);
container.MapPolicy<TImplementation>(policyName);
}
This allows you to make the registration and mapping in one swoop:
container.RegisterWithPolicy<IHistoricalAggregates, PolygonIoApiFetcher>(
"RetryPolicy",
Lifestyle.Singleton);
One last note. In your question you call the IHttpClientFactory.CreateClient
method from within the constructor, and store the client in the consumer. This is typically not a good idea, because it could possibly allow the client to live for a long time and defeats the purpose of the IHttpClientFactory
. Instead, store the IPollyHttpClientFactory
(or IHttpClientFactory
) in a private field and only call its CreateClient
method from within the methods of the consumer, and dispose the client at the end of the method.