Injecting Single Instance HttpClient with specific HttpMessageHandler

You think to complicated. All you need is a HttpClient factory or accessor with a HttpClient property and use it the same way the ASP.NET Core is allowing HttpContext to be injected

public interface IHttpClientAccessor 
{
    HttpClient Client { get; }
}

public class DefaultHttpClientAccessor : IHttpClientAccessor
{
    public HttpClient Client { get; }

    public DefaultHttpClientAccessor()
    {
        Client = new HttpClient();
    }
}

and inject this in your services

public class MyRestClient : IRestClient
{
    private readonly HttpClient client;

    public MyRestClient(IHttpClientAccessor httpClientAccessor)
    {
        client = httpClientAccessor.Client;
    }
}

registration in Startup.cs:

services.AddSingleton<IHttpClientAccessor, DefaultHttpClientAccessor>();

For unit-testing, just mock it

// Moq-esque

// Arrange
var httpClientAccessor = new Mock<IHttpClientAccessor>();
var httpHandler = new HttpMessageHandler(..) { ... };
var httpContext = new HttpContext(httpHandler);

httpClientAccessor.SetupGet(a => a.Client).Returns(httpContext);

// Act
var restClient = new MyRestClient(httpClientAccessor.Object);
var result = await restClient.GetSomethingAsync(...);

// Assert
...

Adding to the conversation from the comments looks like you would need a HttpClient factory

public interface IHttpClientFactory {
    HttpClient Create(string endpoint);
}

and the implementation of the core functionality could look something like this.

public class DefaultHttpClientFactory : IHttpClientFactory, IDisposable
{
    private readonly ConcurrentDictionary<string, HttpClient> _httpClients;

    public DefaultHttpClientFactory()
    {
        this._httpClients = new ConcurrentDictionary<string, HttpClient>();
    }

    public HttpClient Create(string endpoint)
    {
        if (this._httpClients.TryGetValue(endpoint, out var client))
        {
            return client;
        }

        client = new HttpClient
        {
            BaseAddress = new Uri(endpoint),
        };

        this._httpClients.TryAdd(endpoint, client);

        return client;
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        foreach (var httpClient in this._httpClients)
        {
            httpClient.Value.Dispose();
        }
    }
}

That said, if you are not particularly happy with the above design. You could abstract away the HttpClient dependency behind a service so that the client does not become an implementation detail.

That consumers of the service need not know exactly how the data is retrieved.


My current preference is to derive from HttpClient once per target endpoint domain and make it a singleton using dependency injection rather than use HttpClient directly.

Say I am making HTTP requests to example.com, I would have an ExampleHttpClient that inherits from HttpClient and has the same constuctor signature as HttpClient allowing you to pass and mock the HttpMessageHandler as normal.

public class ExampleHttpClient : HttpClient
{
   public ExampleHttpClient(HttpMessageHandler handler) : base(handler) 
   {
       BaseAddress = new Uri("http://example.com");

       // set default headers here: content type, authentication, etc   
   }
}

I then set ExampleHttpClient as singleton in my dependency injection registration and add a registration for HttpMessageHandler as transient as it will be created just once per http client type. Using this pattern I do not need to have multiple complicated registrations for HttpClient or smart factories to build them based on destination host name.

Anything that needs to talk to example.com should take a constructor dependency on ExampleHttpClient and then they all share the same instance and you get connection pooling as designed.

This way also gives you a nicer place to put stuff like default headers, content types, authorisation, base address etc, and helps prevent http config for one service leaking to another service.