Search code examples
c#asp.net.netasp.net-corepolly

How to Register Polly on a IHttpClient Already Registered


Using ASP.NET Core 3, I have a library providing access to a web API that registers IHttpClient like this

services.AddHttpClient<OntraportHttpClient>();
services.AddHttpClient<IOntraportPostForms, OntraportPostForms>();

I do not think it would be good practice to make that library dependent on Polly, so Polly must be configured in the ASP.NET application using it.

Normally, I would register it this way, but it doesn't work since it is already registered.

services.AddHttpClient<OntraportHttpClient>()
    .AddTransientHttpErrorPolicy(p =>
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

Calling the library to register itself after registering the IHttpClient class won't work either because there's no TryAddHttpClient method.

What's the recommended approach to deal with this kind of situation?

Also, in my case, the library is using 2 IHttpClient to access 2 different APIs, do I have to configure Polly twice?


Solution

  • Afaik, the Microsoft implementation of HttpClientFactory does not offer a direct API to edit/append-to an already-defined logical HttpClient (something like services.AppendToHttpClientDefinition<T>(...)). You could ask on the relevant Microsoft github repo.

    Three possible approaches to the overall goal:

    1 Have the library registering OntraportHttpClient allow extra config to be passed in

    Extend the way the library (this?) registers OntraportHttpClient so that external code can pass in an Action<IHttpClientBuilder> additionalConfig.

    The registering code in the library would then be:

    var ontraportHttpClientbuilder = services.AddHttpClient<OntraportHttpClient>();
    additionalConfig(ontraportHttpClientbuilder);
    

    The library would have no dependency on Polly, but the Action<IHttpClientBuilder> additionalConfig you pass in can configure Polly policies. Outside the library you can declare and pass in:

    Action<IHttpClientBuilder> additionalConfig = (builder) => builder.AddTransientHttpErrorPolicy(...);
    

    2 Intercept the way HttpClientFactory configures HttpClients

    If you prefer not to add any extra public configuration hook to OntraportApi.NET, you could:

    2a. Use IConfigureNamedOptions<HttpClientFactoryOptions> outside the library, to dynamically change the configuration for OntraportHttpClient, something like:

    services.AddSingleton<IConfigureOptions<HttpClientFactoryOptions>, DynamicHttpClientFactoryConfiguration>();
    

    where:

    internal class DynamicHttpClientFactoryConfiguration : IConfigureNamedOptions<HttpClientFactoryOptions>
    {
        public void Configure(String httpClientName, HttpClientFactoryOptions options)
        {
            if (httpClientName == nameof(OntraportHttpClient))
            {
                options.HttpMessageHandlerBuilderActions.Add(httpMessageHandlerBuilder =>
                {
                    httpMessageHandlerBuilder.AdditionalHandlers.Add(pollyDelegatingHandler);
                });
            }
        }
    } 
    

    where pollyDelegatingHandler is some PolicyHttpMessageHandler you have constructed separately.

    Or very similarly

    2b. Use an IHttpMessageHandlerBuilderFilter, eg:

    services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, DynamicMessageHandlerBuilderFilter>());
    

    where:

    internal class DynamicMessageHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter
    {
        public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
        {
            return (builder) =>
            {
                next(builder);
    
                if (builder.Name == nameof(OntraportHttpClient))
                {
                    builder.AdditionalHandlers.Add(pollyDelegatingHandler);
                }
            };
        }
    } 
    

    3 Use Polly outside OntraportHttpClient

    Polly can be used with anything; it doesn't have to be used only within HttpClients via HttpClientFactory. So you could

    var foo = await policy.ExecuteAsync<Foo>(async () => await ontraportClient.GetAsync<Foo>(/* etc */));
    

    for some IAsyncPolicy policy.

    From a quick read of OntraportHttpClient, a pattern like this should work, but it is marginally less efficient (and won't work for co-operative cancellation eg by TimeoutPolicy, if OntraportHttpClient doesn't support CancellationTokens).