Last Updated: 27 September 2022
The problems with the HttpClient surrounding socket exhaustion and DNS updates are well documented. For years, we as developers have known to create a single instance of HttpClient and re-use it throughout the entire application lifetime. This wasn't necessarily the end of it though, but thankfully from .Net Core 2.1 onwards, Microsoft added IHttpClientFactory to configure and handle HttpClient instances. This can be found in the Microsoft.Extensions.Http Nuget Package.
Using this factory has many benefits and are a few different ways to use it. We recommend two of these methods:
Register the factory in Startup.cs (or wherever you are defining your dependencies) and then add IHttpClientFactory to the constructor of your class. Whenever you need an HttpClient, simply call CreateClient()
services.AddHttpClient();
public class UsingFactory : IUsingFactory
{
private readonly IHttpClientFactory _httpClientFactory;
public UsingFactory(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
}
public void DoSomething()
{
var httpClient = _httpClientFactory.CreateClient();
// use ParkSquare.Extensions.Http methods on httpClient to call API endpoints
}
}
With a typed client, the HttpClient and HttpMessageHandler can be pre-configured just for that particular class. In this example, the class we want the HttpClient injecting into is called TypedClient, which implements the ITypedClient interface. Most of the configuration shown here is optional, to illustrate what can be done. Note the way in which a configuration class (TypedClientConfig) is retrieved from the DI container.
services.AddSingleton<ITypedClientConfig, TypedClientConfig>();
services.AddHttpClient<ITypedClient, TypedClient>()
.ConfigureHttpClient((serviceProvider, httpClient) =>
{
var clientConfig = serviceProvider.GetRequiredService<ITypedClientConfig>();
httpClient.BaseAddress = clientConfig.BaseUrl;
httpClient.Timeout = TimeSpan.FromSeconds(clientConfig.Timeout);
httpClient.DefaultRequestHeaders.Add("User-Agent", "BlahAgent");
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
})
.SetHandlerLifetime(TimeSpan.FromMinutes(5)) // Default is 2 mins
.ConfigurePrimaryHttpMessageHandler(x =>
new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
UseCookies = false,
AllowAutoRedirect = false,
UseDefaultCredentials = true,
});
public class TypedClient : ITypedClient
{
private readonly HttpClient _httpClient;
public TypedClient(HttpClient httpClient)
{
_httpClient = httpClient;
}
public void DoSomething()
{
// Read some properties to prove the intended HttpClient has been injectd in
Console.WriteLine($"Base address of injected client: {_httpClient.BaseAddress}");
Console.WriteLine($"Timeout of injected client: {_httpClient.Timeout}");
// use ParkSquare.Extensions.Http methods on _httpClient to call API endpoints
}
}
public class TypedClientConfig : ITypedClientConfig
{
public TypedClientConfig(IConfiguration configuration)
{
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
configuration.Bind("TypedClient", this);
}
public Uri BaseUrl { get; set; }
public int Timeout { get; set; }
}
{
"TypedClient": {
"BaseUrl": "https://www.example.com",
"Timeout": 10
}
}