How to Use HTTPClientFactory in .NET Core - Windows ASP.NET Core Hosting 2024 | Review and ComparisonWindows ASP.NET Core Hosting 2024 | Review and Comparison

If you’re developing a.NET application, it’s likely that you’ll need to call an external API via HTTP.

To send HTTP requests in.NET, use the HttpClient. And it’s a fantastic abstraction to work with, especially with the methods that support JSON payloads and responses.

Unfortunately, it is easy to abuse the HttpClient.

Port exhaustion and DNS behavior are among the most common issues.

Here’s what you should know about using HttpClient:

  • How not to use HttpClient
  • How to simplify configuration with IHttpClientFactory
  • How to configure typed clients
  • Why you should avoid typed clients in singleton services
  • When to use which option

The Simple Way to Use HttpClient

To use the HttpClient, simply create a new instance, configure its properties, and send requests.

What could possibly go wrong?

HttpClient instances are designed to be long-lived and reused throughout the application’s lifecycle.

Each instance has its own connection pool for isolation and to prevent port exhaustion. If a server is under a lot of load and your application is constantly creating new connections, the available ports may run out. This will result in an exception at runtime when attempting to send a request.

So, how do you avoid this?

public class GitHubService
{
    private readonly GitHubSettings _settings;

    public GitHubService(IOptions<GitHubSettings> settings)
    {
        _settings = settings.Value;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        var client = new HttpClient();

        client.DefaultRequestHeaders.Add("Authorization", _settings.GitHubToken);
        client.DefaultRequestHeaders.Add("User-Agent", _settings.UserAgent);
        client.BaseAddress = new Uri("https://api.github.com");

        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}

The Smart Way To Create An HttpClient Using IHttpClientFactory

Instead of managing the HttpClient lifetime yourself, you can use an IHttpClientFactory to create the HttpClient instance.

Simply call the CreateClient method and use the returned HttpClient instance to send your HTTP requests.

Why is this a better approach?

The expensive part of the HttpClient is the actual message handler – HttpMessageHandler. Each HttpMessageHandler has an internal HTTP connection pool that can be reused.

The IHttpClientFactory will cache the HttpMessageHandler and reuse it when creating a new HttpClient instance.

An important note here is that HttpClient instances created by IHttpClientFactory are meant to be short-lived.

public class GitHubService
{
    private readonly GitHubSettings _settings;
    private readonly IHttpClientFactory _factory;

    public GitHubService(
        IOptions<GitHubSettings> settings,
        IHttpClientFactory factory)
    {
        _settings = settings.Value;
        _factory = factory;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        var client = _factory.CreateClient();

        client.DefaultRequestHeaders.Add("Authorization", _settings.GitHubToken);
        client.DefaultRequestHeaders.Add("User-Agent", _settings.UserAgent);
        client.BaseAddress = new Uri("https://api.github.com");

        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}

Reducing Code Duplication for Named Clients

Using IHttpClientFactory eliminates most of the challenges of manually creating a HttpClient. When creating a new HttpClient using the CreateClient method, it is necessary to configure the default request parameters.

To configure a named client, use the AddHttpClient method and specify the desired name. The AddHttpClient method accepts a delegate for configuring the default parameters of the HttpClient instance.

services.AddHttpClient("github", (serviceProvider, client) =>
{
    var settings = serviceProvider
        .GetRequiredService<IOptions<GitHubSettings>>().Value;

    client.DefaultRequestHeaders.Add("Authorization", settings.GitHubToken);
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);

    client.BaseAddress = new Uri("https://api.github.com");
});

The main difference is that you now need to obtain the client by passing the client’s name to CreateClient.

However, the use of the HttpClient appears much simpler:

public class GitHubService
{
    private readonly IHttpClientFactory _factory;

    public GitHubService(IHttpClientFactory factory)
    {
        _factory = factory;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        var client = _factory.CreateClient("github");

        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}

Replace Named Clients with Typed Clients

The disadvantage of using named clients is that you have to resolve a HttpClient by passing in a name each time.

There is a better way to achieve the same result: configure a typed client. To do this, use the AddClient method and configure the service that will consume the HttpClient.

Under the hood, this is still using a named client, with the same name as the type.

This will also register GitHubService with a transient lifetime.

services.AddHttpClient<GitHubService>((serviceProvider, client) =>
{
    var settings = serviceProvider
        .GetRequiredService<IOptions<GitHubSettings>>().Value;

    client.DefaultRequestHeaders.Add("Authorization", settings.GitHubToken);
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);

    client.BaseAddress = new Uri("https://api.github.com");
});

Inside GitHubService, you inject and use the typed HttpClient instance, which will have all of the settings applied.

No more dealing with IHttpClientFactory or manually creating HttpClient instances.

public class GitHubService
{
    private readonly HttpClient client;

    public GitHubService(HttpClient client)
    {
        _client = client;
    }

    public async Task<GitHubUser?> GetUserAsync(string username)
    {
        GitHubUser? user = await client
            .GetFromJsonAsync<GitHubUser>($"users/{username}");

        return user;
    }
}

Why Should You Avoid Typed Clients in Singleton Services?

Injecting a typed client into a singleton service may cause issues. Because the typed client is transient, injecting it into a singleton service will cause it to be cached for the duration of the singleton service.

This prevents the typed client from responding to DNS changes.

The recommended approach for using a typed client in a singleton service is to use SocketsHttpHandler as the primary handler and configure the PooledConnectionLifetime.

Because the SocketsHttpHandler will handle connection pooling, you can turn off recycling at the IHttpClientFactory level by setting HandlerLifetime to Timeout.InfiniteTimeSpan.

services.AddHttpClient<GitHubService>((serviceProvider, client) =>
{
    var settings = serviceProvider
        .GetRequiredService<IOptions<GitHubSettings>>().Value;

    client.DefaultRequestHeaders.Add("Authorization", settings.GitHubToken);
    client.DefaultRequestHeaders.Add("User-Agent", settings.UserAgent);

    client.BaseAddress = new Uri("https://api.github.com");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new SocketsHttpHandler()
    {
        PooledConnectionLifetime = TimeSpan.FromMinutes(15)
    };
})
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);

Final Verdict – When Should You Use Which Option?

I showed you a few possible options for working with HttpClient.

But which one should you use and when?

Microsoft was kind enough to provide us with a set of best practices and recommended use for HttpClient.

  • Use a static or singleton HttpClient instance with a PooledConnectionLifetime configured, since this solves both port exhaustion and tracking DNS changes
  • Use IHttpClientFactory if you want to move the configuration to one place, but remember that clients are meant to be short-lived
  • Use a typed client if you want the IHttpClientFactory configurability