Skip to content

HttpClient

HttpClient is a class in dotnet that allows you to make HTTP requests, for example for accessing a REST API. There are a few things you should know when working with the HttpClient class.

var httpClient = new HttpClient();
var response = await httpClient.GetAsync("https://rest-api.com/resource");
var body = await response.Content.ReadAsStringAsync();

The above code creates a new HttpClient and uses it to make a GET request to the specified URL. The response contains all the information from the HTTP response. We could then access the response body by calling Content.ReadAsStringAsync().

There are a few problems with the above code:

HttpClient is disposable

The HttpClient implements the IDisposable interface. Everything that is disposable should be disposed after usage. This let’s the class clean up certain allocated resources like connections or memory. By using the using keyword, we make sure that the object is disposed when leaving the current code-block.

using var httpClient = new HttpClient();
var response = await httpClient.GetAsync("https://rest-api.com/resource");
var body = await response.Content.ReadAsStringAsync();
// alternatively
using (var httpClient = new HttpClient())
{
var response = await httpClient.GetAsync("https://rest-api.com/resource");
var body = await response.Content.ReadAsStringAsync();
}

However creating a new HttpClient object for every request is also not a good idea.

Socket exhaustion

Each HttpClient instance opens its own socket connection to the target server. Creating multiple instances quickly consumes available sockets and system resources, leading to socket exhaustion. This can cause SocketException errors and potentially bring down the application.

This is why it is often recommended, that you should only create a singe HttpClient object in your application and reuse that object. This can be done by using a Singleton or simply a static field.

private static HttpClient HttpClient = new HttpClient();
public void AccessRestApi() {
var response = await HttpClient.GetAsync("https://rest-api.com/resource");
var body = await response.Content.ReadAsStringAsync();
}

Base Address

When you use the HttpClient to access different resources on the same server you can set a BaseAddress.

private static HttpClient HttpClient = new HttpClient()
{
BaseAddress = new Uri("https://rest-api.com/");
};
public void AccessRestApi() {
var response = await HttpClient.GetAsync("resource");
var body = await response.Content.ReadAsStringAsync();
}
public void AccessRestApi2() {
var response = await HttpClient.GetAsync("resource2");
var body = await response.Content.ReadAsStringAsync();
}

DNS Issues

By reusing the same HttpClient instance we face another issue. The HttpClient has no built in mechanism to deal with DNS updates. So when the DNS entry that was resolved initially changes and points to a new IP, our HttpClient will never notice that and still be trying to request the server on the initially resolved IP until our application is restarted.

private static readonly HttpClient HttpClient = new(new SocketHttpsHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(1)
})
{
BaseAddress = new Uri("https://rest-api.com/");
};

This code allows the HttpClient to deal with DNS changes and update its cache.

Using HttpClient with Dependency Injection

Program.cs
builder.Services.AddHttpClient<IWeatherClient, OpenWeatherClient>(client =>
{
client.BaseAddress = new Uri("https://api.openweathermap.org/data/2.5/");
});
// OpenWeatherClient.cs
public class OpenWeatherClient : IWeatherClient {
private readonly HttpClient _httpClient;
public OpenWeatherClient(HttpClient httpClient) {
_httpClient = httpClient;
}
}

By using the AddHttpClient method to register the HttpClient, the HttpClient will be transient, meaning every time it gets injected we get a new instance. But the HttpClientHandler used in the background to do the heavy lifting is actually pooled and managed by an HttpClientFactory and it gives us a client with a reused handler. This solves all the above problems like socket exhaustion or DNS issues.

Using a named client

Alternatively to the above method, you can use a named client.

Program.cs
builder.Services.AddHttpClient("weatherapi", client =>
{
client.BaseAddress = new Uri("https://api.openweathermap.org/data/2.5/");
});
// OpenWeatherClient.cs
public class OpenWeatherClient : IWeatherClient {
private readonly IHttpClientFactory _httpClientFactory;
public OpenWeatherClient(IHttpClientFactory httpClientFactory) {
_httpClientFactory = httpClientFactory;
}
public async Task<string> GetCurrentWeatherForCity(string city) {
var client = _httpClientFactory.CreateClient("weatherapi");
var response = await client.GetAsync($"weather?q={city}");
return await response.Content.ReadAsStringAsync();
}
}

Dealing with JSON data

Very often you will send and receive JSON data when using the HttpClient. You can use pre-existing methods for accessing and sending JSON, that will automatically be converted from/to your structured objects in C#.

WeatherData data = await client.GetFromJsonAsync<WeatherData>("weather");
await client.PostAsJsonAsync("weather", data);