Up until now, we’ve been using HttpClient directly in our services. In every service, we’ve created an HttpClient instance and all the required configurations. That lead to repeating code in all of our service classes. Well, in this article we are going to learn how to prevent that by using HttpClientFactory. Of course, this is not the only advantage of using HttpClientFactory. We are going to learn how HttpClientFactory prevents additional problems that HttpClient can cause. Additionally, we are going to show you how to create named and typed clients using HttpClientFactory.

To download a source code, you can visit our HttpClientFactory repository.

You can also visit our HttpClient Tutorial page, to see all the articles from this tutorial.

We are going to divide this article into the following sections:

Let’s dive right into it.

Problems with HttpClient

The HttpClient class implements the IDisposable interface. By seeing that, we can all be tempted to try using our HttpClient instance inside the using directive, thus disposing of it once it is out of the scope. But, this is not a good practice. If we dispose of the HttpClient, we are going to dispose of the underlying HttpClientHandler as well. Now, this means that for every new request we have to create a new HttpClient instance and thus the handler as well. Of course, that’s the problem. Reopening connections could lead to slow performance because these connections and HttpClientHandlers are pretty expensive while working with HttpClient.

Also, there is another problem. By creating too many connections, we can face socket exhaustion because we use too many sockets too fast, and we don’t have any available socket to create a new connection.

So, with all these in mind, we can conclude that we shouldn’t dispose of our HttpClient but share it throughout the requests. That’s what we’ve been doing in our previous articles with the static HttpClient instance. This also allows reusing of the underlying connections.

But, we have to pay attention, that using a static instance is not the ultimate solution. When we reuse our instance, we also reuse the connection until the socket is closed. Thus connections won’t get the update from DNS (for example switching between different environments). If our connection is not aware of switching from staging to the production environment, our requests would not go to the right environment as well.

To help us solve these problems, we can use HttpClientFactory to create HttpClient instances.

How HttpClientFactory Helps Us Solving Mentioned Problems?

Not only that HttpClientFactory can create and manage new HttpClient instances but also, it works with underlying handlers. Basically, when creating new HttpClient instances, it doesn’t recreate a new message handler but it takes one from a pool. Then, it uses that message handler to send the requests to the API. The default lifetime of a handler is set to two minutes, and during that time, any request for a new HttpClient can reuse an existing message handler and the connection as well. This means we don’t have to create a new message handler for every request and also we don’t have to open a new connection, thus preventing the socket exhausting issue.

Furthermore, since the handler’s lifetime is set to two minutes, after that time, HttpClientFactory uses a new message handler. Well, with that, we solve the DNS issues. When we use a new message handler, we take all the DNS changes into account.

In addition to solving these problems, with HttpClientHandler, we can centralize our HttpClient’s configuration. If you read our previous articles from this series, you saw that we had to repeat the same configuration in each service class. Well, with HttpClientHandler, we can prevent that.

That said, let’s see how we can use HttpClientFactory.

Adding HttpClientFactory in Our ASP.NET Core Application

To be able to use HttpClientFactory in our app, we have to install the Microsoft.Extensions.Http library in our client application:

Install-Package Microsoft.Extensions.Http -Version 5.0.0

Then, we have to add the IHttpClientFactory and other services to the service collection by using the AddHttpClient method in the Program class:

private static void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient();

    //services.AddScoped<IHttpClientServiceImplementation, HttpClientCrudService>();
    //services.AddScoped<IHttpClientServiceImplementation, HttpClientPatchService>();
    //services.AddScoped<IHttpClientServiceImplementation, HttpClientStreamService>();
    //services.AddScoped<IHttpClientServiceImplementation, HttpClientCancellationService>();
}

For now, this is enough. We are going to expand this method with additional configuration soon enough.

Now, let’s create a new service class as we did in our previous articles:

public class HttpClientFactoryService : IHttpClientServiceImplementation
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly JsonSerializerOptions _options;

    public HttpClientFactoryService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;

        _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
    }

    public async Task Execute()
    {
        throw new NotImplementedException();
    }
}

To be able to use HttpClientFactory in our HttpClientFactoryService class, we have to inject it with Dependency Injection. And that’s exactly what we do. Additionally, we configure options for our JSON serialization. Since we don’t want to add the cancellation logic here, we are not using the CancellationTokenSource as we did in our previous article. Of course, if you want, you can add it here without a problem.

Now, let’s add a new method to fetch companies from the API:

private async Task GetCompaniesWithHttpClientFactory()
{
    var httpClient = _httpClientFactory.CreateClient();

    using (var response = await httpClient.GetAsync("https://localhost:5001/api/companies", HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();

        var stream = await response.Content.ReadAsStreamAsync();

        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
    }
}

In this code, the only thing we are not familiar with is the part where we use the CreateClient method from HttpClientFactory to create a new HttpClient using the default configuration. Everything else is already explained in our previous articles from this series. Also, since we didn’t provide our custom configuration for the factory, we have to use an entire URI for the companies endpoint in the GetAsync method.

After this, we can modify the Execute method:

public async Task Execute()
{
    await GetCompaniesWithHttpClientFactory();
}

And also, let’s register this service in the Program class:

private static void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient();

    ...
    services.AddScoped<IHttpClientServiceImplementation, HttpClientFactoryService>();
}

As soon as we do that, we can place the breakpoint in our new method and start both applications:

Using HttpClientFactory to fetch data and create a client

And there we go. We can see all three companies as a result. But, we can improve this solution by using Named and Typed HttpClient instances.

Using Named HttpClient Instances

In the Program class, we use the AddHttpClient method to register IHttpClientFactory without additional configuration. This means that every HttpClient instance we create with the CreateClient method will have the same configuration. But usually, that is not enough, since our client app often requires different HttpClient instances while communicating with a single or multiple APIs. Well, to support that, we can use the named HttpClient instances.

In our previous articles, we used the same configuration in each service to set up the base address, timeout, and to clear default request headers. Now, we can do the same, but only in one place:

private static void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("CompaniesClient", config =>
    {
        config.BaseAddress = new Uri("https://localhost:5001/api/");
        config.Timeout = new TimeSpan(0, 0, 30);
        config.DefaultRequestHeaders.Clear();
    });

    ...
    services.AddScoped<IHttpClientServiceImplementation, HttpClientFactoryService>();
}

With these modifications, the AddHttpClient method adds IHttpClientFactory to the service collection and also configures a named HttpClient instance. We provide a name for the instance and also a default configuration.

After this, we can modify the method in our new service:

private async Task GetCompaniesWithHttpClientFactory()
{
    var httpClient = _httpClientFactory.CreateClient("CompaniesClient");

    using (var response = await httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();

        var stream = await response.Content.ReadAsStreamAsync();

        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
    }
}

As you can see, we pass a name argument to the CreateClient method and also we don’t have to use a full URI in the GetAsync method. Since we are using the name of the client, the configuration that corresponds to this name applies as well.

Once we start both applications, we are going to get the same result as before:

Using HttpClientFactory to fetch data and create a client

Excellent.

Now, let’s see how to use a Typed client.

Using Typed HttpClient Instances

With the typed instances, we can achieve the same thing as with named instances, but we don’t have to use strings during the registration – we can use types.

Let’s start by creating a new Clients folder in a client application and a new CompaniesClient class inside that folder:

public class CompaniesClient
{
    public HttpClient Client { get; }

    public CompaniesClient(HttpClient client)
    {
        Client = client;

        Client.BaseAddress = new Uri("https://localhost:5001/api/");
        Client.Timeout = new TimeSpan(0, 0, 30);
        Client.DefaultRequestHeaders.Clear();
    }
}

This is our typed client class with the default configuration, and we can register it in the Program class by calling AddHttpClientΒ one more time in the ConfigureServices method:

services.AddHttpClient<CompaniesClient>();

So, we are not using the name but the type of the client.

Now, in our HttpClientFactoryService, we have to inject our new client:

private readonly IHttpClientFactory _httpClientFactory;
private readonly CompaniesClient _companiesClient;
private readonly JsonSerializerOptions _options;

public HttpClientFactoryService(IHttpClientFactory httpClientFactory, CompaniesClient companiesClient)
{
    _httpClientFactory = httpClientFactory;
    _companiesClient = companiesClient;

    _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
}

Then, we are going to create a new method to make use of our typed client:

private async Task GetCompaniesWithTypedClient()
{
    using (var response = await _companiesClient.Client.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();

        var stream = await response.Content.ReadAsStreamAsync();

        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);
    }
}

As we can see, we are not creating a new client instance by using the CreateClient method. This time, we just use the injected typed client with its Client property.

Finally, let’s just execute this method:

public async Task Execute()
{
    //await GetCompaniesWithHttpClientFactory();
    await GetCompaniesWithTypedClient();
}

Once we start both applications, we are going to get the same result as before:

Using HttpClientFactory to fetch data and create a client

This looks great.

Now let’s see how to extract the company related logic to the CompaniesClient class.

Encapsulation of the Logic Related to a Typed Client

Since we already have the typed client class, we can extract all the related logic from the service to this class. To do that, we are going to modify the CompaniesClient class:

public class CompaniesClient
{
    private readonly HttpClient _client;
    private readonly JsonSerializerOptions _options; 
    
    public CompaniesClient(HttpClient client)
    {
        _client = client;

        _client.BaseAddress = new Uri("https://localhost:5001/api/");
        _client.Timeout = new TimeSpan(0, 0, 30);
        _client.DefaultRequestHeaders.Clear();

        _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; 
    }
}

We don’t have the Client property anymore. Instead, we have a private readonly variable, which we are going to use in this class to execute the HttpClient’s logic. Additionally, we add the JsonSerializerOptions configuration.

Now, we can add a new method:

public async Task<List<CompanyDto>> GetCompanies()
{
    using (var response = await _client.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead))
    {
        response.EnsureSuccessStatusCode();

        var stream = await response.Content.ReadAsStreamAsync();

        var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options);

        return companies;
    }
}

With this method, we are fetching the companies from the API and return a result.

Finally, we can modify the GetCompaniesWithTypedClientmethod in a service class:

private async Task GetCompaniesWithTypedClient() => await _companiesClient.GetCompanies();

Excellent.

The logic related to the typed client is in a typed client’s class and our service is just calling that method.

You can test the execution, but you will get the same result as before.

Conclusion

To sum up, in this article, we have learned:

  • What problems HttpClientFactory solves
  • How to use HttpClientFactory in our application
  • The way to use Named and Typed client instances
  • And how to extract logic from a service to a client class

We’ve learned a lot in this article and in the entire series.

Until the next article.

All the best.