Search code examples
c#asp.net-core.net-coredotnet-httpclient

AddHttpClient<TClient,TImplementation> registers as transient, can it handle high volume concurrent requests efficiently?


I noticed that when using ASP.NET Core's IHttpClientFactory, the typed client registration method AddHttpClient<TClient,TImplementation> does two things:

  1. It registers DI for <TClient,TImplementation> as transient, as if calling services.AddTransient<TClient,TImplementation> in startup.cs

  2. It will inject a HttpClient instance of this registered type for each object initiated.

My concern is, if this is configured as transient, will it be able to handle a large number of concurrent TImplementation objects making http calls, because there will be a new HttpClient as well as a new TClient created for every call? These clients will all access the same URL, will sockets be re-used properly?


Solution

  • As King King has already pointed out the HttpMessageHandler which matters.

    To better understanding how does it work I suggest to examine the DefaultHttpClientFactory's source code.

    Let's take a look at the CreateClient method:

    public HttpClient CreateClient(string name)
    {
        if (name == null)
        {
            throw new ArgumentNullException(nameof(name));
        }
    
        HttpMessageHandler handler = CreateHandler(name);
        var client = new HttpClient(handler, disposeHandler: false);
    
        HttpClientFactoryOptions options = _optionsMonitor.Get(name);
        for (int i = 0; i < options.HttpClientActions.Count; i++)
        {
            options.HttpClientActions[i](client);
        }
    
        return client;
    }
    

    As you can see it calls CreateHandler:

    public HttpMessageHandler CreateHandler(string name)
    {
        if (name == null)
        {
            throw new ArgumentNullException(nameof(name));
        }
    
        ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
    
        StartHandlerEntryTimer(entry);
    
        return entry.Handler;
    }
    

    Here we have a pool of handlers via _activeHandlers. And a factory method _entryFactory, which is called when a given entry does not exist. Let's take a look at their definitions:

    _activeHandlers = new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal);
    _entryFactory = (name) =>
    {
        return new Lazy<ActiveHandlerTrackingEntry>(() =>
        {
            return CreateHandlerEntry(name);
        }, LazyThreadSafetyMode.ExecutionAndPublication);
    };
    
    _expiredHandlers = new ConcurrentQueue<ExpiredHandlerTrackingEntry>();
    

    So, as you can see it uses a Lazy structure to minimize the cost of the initializations.

    The related CreateHandlerEntry's source code can be found here if you are interested.

    I also suggest to read Stephen Gordon's excellent article about this topic.