question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add an overload with a Func<IServiceProvider, T> to AddHttpClient<TClient, TImplementation>

See original GitHub issue

We have a multi-tenant web app that calls to our multi-tenant Web Api. I’m changing the api client used in the web app to use IHttpClientFactory, using typed clients. However, as it’s a multi-tenant software, we have logic to call to different URLs depending on where the request is coming from. Something like this:

public class MyApiClient : IMyApiClient
{
   public MyApiClient(HttpClient client, string baseUrl) {}
}

I used to do DI like this before:

services.AddScoped<IMyApiClient>(sp => 
{
   // decide which url to use in the client
   return new MyApiClient(url);
}

However, now with .AddHttpClient, since there’s no overload that accepts a Func<IServiceCollection, T>. there’s no way I can do that anymore, and I’m left with three non-optimal solutions:

  1. Change to use named clients instead of typed clients. However, that’s not good not only because well, it’s named clients, but also because I already have plenty of dependencies to IMyApiClient in a lot of places.

  2. Copy the source code from .AddHttpClient and do the base URL selection just like before.

  3. Create a whole new class just to represent the API base URL so I can register it on the IoC container and ask for that instead of a string on the MyApiClient constructor.

Note that I can’t just set the HttpClient base URL using the Action<HttpClient> configureClient overload for two reasons:

  1. In order to select the base URL, I need another dependency. Thus, I would need an instance of IServiceProvider.
  2. Even if I could change my logic to not need any dependency when selecting the base URL, it still wouldn’t work, because the base URL can’t be changed after the HttpClient has executed a request.

EDIT:

I ended up going with option 2. This is what I came up with:

services.AddHttpClient("MyApiClient")
    .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(4, attempt => TimeSpan.FromSeconds(Math.Pow(1.45, attempt)) + TimeSpan.FromMilliseconds(_xoroshiro.Next(0, 100))))
    .AddTransientHttpErrorPolicy(p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(15)));

services.AddTransient<IMyApiClient>(sp =>
{
    // logic to select URL
    var context = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;
    string[] host = context.Request.Host.Host.Replace("someurl", "").Split(new char[] { '.' });
    string subdomain= host.Length > 1 ? host[1] : host[0];
    string url = Configuration.GetSection("MyApiBaseUrl")?[subdomain] ?? Configuration.GetSection("MyApiBaseUrl")?["Default"];

    // actual DI logic
    var httpClientFactory = sp.GetRequiredService<IHttpClientFactory>();
    var httpClient = httpClientFactory.CreateClient("MyApiClient");

    return new MyApiClient(url, httpClient);
});

What’s different from the ASP.NET implementation is that I don’t use .GetRequiredService<ITypedHttpClientFactory<TClient>>(); and .Activator(_services, new object[] { httpClient });, instead just newing the api client up. Is there any practical difference to how this will behave?

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

4reactions
rynowakcommented, Feb 10, 2020

Here’s the API proposal to review.

Summary

This change adds an accelerator for some functionality that’s hard to discover. We’ve had a few bugs reported now to the effect of “HTTP Client Factory doesn’t have what I need” people people didn’t find these APIs where they are currently exposed.

Before

services.AddHttpClient("widget").AddTypedClient<IWidgetClient, WidgetClient>((httpClient, services) =>
{
    var client = new WidgetClient(httpClient, services.GetRequiredService<ILogger<WidgetClient>>());
    client.SomeSetting = Configuration["MySetting"];
    return client;
});

After

services.AddHttpClient<IWidgetClient, WidgetClient>((httpClient, services) =>
{
    var client = new WidgetClient(httpClient, services.GetRequiredService<ILogger<WidgetClient>>());
    client.SomeSetting = Configuration["MySetting"];
    return client;
});

APIs

note: the TImplementation type parameter is needed to avoid a really nasty source-breaking change. This means that practically speaking, both type parameters will need to be specified. I think this still accomplishes the goal of making these APIs more discoverable because they will be part of the overload set of AddHttpClient (today they are hiding on AddTypedClient where folks don’t look)

namespace Microsoft.Extensions.DependencyInjection
{
    public static class HttpClientFactoryServiceCollectionExtensions
    {
        public static IHttpClientBuilder AddHttpClient<TClient, TImplementation>(this IServiceCollection services, Func<HttpClient, TImplementation> factory)
            where TClient : class
            where TImplementation : class, TClient
        {
        }

        public static IHttpClientBuilder AddHttpClient<TClient, TImplementation>(this IServiceCollection services, string name, Func<HttpClient, TImplementation> factory)
            where TClient : class
            where TImplementation : class, TClient
        {
        }

        public static IHttpClientBuilder AddHttpClient<TClient, TImplementation>(this IServiceCollection services, Func<HttpClient, IServiceProvider, TImplementation> factory)
            where TClient : class
            where TImplementation : class, TClient
        {
        }

        public static IHttpClientBuilder AddHttpClient<TClient, TImplementation>(this IServiceCollection services, string name, Func<HttpClient, IServiceProvider, TImplementation> factory)
            where TClient : class
            where TImplementation : class, TClient
        {
        }
    }
}
1reaction
SylvesterH13commented, Apr 9, 2019

I’ve got a similar problem. I need to send requests authenticated with Digest Authentication. However, in order to do that with HttpClient (and not manually, which is a pain) I need to pass the credentials to the HttpClient constructor. Unfortunately though, there is no oveload of AddHttpClient currently that accepts a Func<HttpClient> (only Action<HttpClient> for configuration).

Read more comments on GitHub >

github_iconTop Results From Across the Web

Add an overload with a Func<IServiceProvider, T> to ...
We have a multi-tenant web app that calls to our multi-tenant Web Api. I'm changing the api client used in the web app...
Read more >
HttpClientFactoryServiceCollecti...
Adds the IHttpClientFactory and related services to the IServiceCollection and configures a binding between the TClient type and a named HttpClient.
Read more >
c# - IServiceCollection does not contain a defintion for ...
cs I am getting this error: "IServiceCollection does not contain a defintion for AddHttpClient". I have already referenced using Microsoft.
Read more >
ASP.NET Core Dependency Injection - Steve Gordon
In this case, we can use an overload of AddSingleton, which accepts a Func<IServiceProvider, object>. This function is called at the time ...
Read more >
Exploring the code behind IHttpClientFactory in depth
In this post I look in depth at the code behind the default implementation of IHttpClientFactory and see how it manages HttpClient handler ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found