When an ASP NET application needs to talk to an external service or API, it needs to make an HTTP Request.
When using ASP.NET to build an application, HTTP requests is made using an instance of the HttpClient class. An HttpClient class acts as a session to send HTTP Requests. It is a collection of settings applied to all requests executed by that instance.
Using the .NET C# HttpClient might seem straightforward. However, some underlying issues go unnoticed until when the application is under a large load. It is also not the best time for you to figure out these issues.
So let's spend some time now and understand the proper way to use HttpClient class and avoid running into issues with it for your application.
Common Issues When Using C# HttpClient
Before we go any further, let's first understand the common issues when using HttpClient and how to uncover them even when running on your local machine without any load.
Below I have a code sample used to talk to an external API, in this case, a weather api, to fetch weather details for a given city. The code instantiates a new instance of HttpClient, makes a GET request to the external API and returns the JSON response.
using(var httpClient = new HttpClient())
{
string APIURL = $"http://api.weatherapi.com/v1/current.json?key={API_KEY}&q={cityName}";
var response = await httpClient.GetAsync(APIURL);
return await response.Content.ReadAsStringAsync();
}
It works fine; happy days. Let's move on to the next feature!
Socket Exhaustion
But wait, let's take a second and fire up the command line. Let's see what's happening behind the scenes with the HttpClient and every execution of the above code.
We will use a popular command-line utility, netstat, to look at the network statistics. It displays all active connections and details of it. Since we want to filter it down by the connections to the Weather API, let's filter it down using the API's IP address.
Running ping api.weather.com
returns the IP address we want - 185.190.83.2
Let’s use that to filter the records returned using the netstat command - netstat -ano | findstr 185.190.83.2
Every request to the API endpoint opens a new connection to the external API. As shown in the image above, you can see more network connections when running the netstat
command after making requests to our API endpoint. Even after the HttpClient connection is disposed, it leaves the network connections in a TIME_WAIT state.
The TIME_WAIT state means the connection is closed on one side (ours), but we''re still waiting to see if any additional packets come in because of a delay in the network connection.
These connections will eventually get closed after a timeout. However, as you can see, if there are many requests to the API, we can soon run of sockets to create (one per connection), and the application will throw an exception. The worst thing is such issues rarely happen in local development or testing unless you perform a load test on the application.
DNS Changes Not Reflecting
If creating a new instance for every request is bad, the first solution that comes to our mind is the Singleton Pattern.
private static HttpClient _httpClient;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
if(_httpClient == null)
_httpClient = new HttpClient();
}
We can create a new instance of HttpClient and not dispose of for the application lifetime. In this case, we reuse the HttpClient instance, and so only one connection is maintained. It works fine as long as there are no DNS or other network-level changes to the external API's connection. If it happens, we will have to restart our API application to create a new HttpClient instance.
You can read more about these issues using the HttpClient class directly in the official Microsoft documentation.
Use IHttpClientFactory To Create HttpClient
Now that we know the issues let’s see how to fix this. The simplest way is to inject the IHttpClient Factory
and create a new HttpClient instance from it.
private readonly IHttpClientFactory _httpClientFactory;
public WeatherForecastController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
[HttpGet]
public async Task<string> Get(string cityName)
{
var httpClient = _httpClientFactory.CreateClient();
string APIURL = $"http://api.weatherapi.com/v1/current.json?key={API_KEY}&q={cityName}";
...
}
To enable Dependency Injection of the IHttpClientFactory
instance we need to make sure to call services.AddHttpClient()
method in ConfigureServices
method of Startup.cs
.
Using the IHttpClientFactory has several benefits, including managing the lifetime of the network connections. Using the factory to create the client reuses connection from a connection pool, thereby not creating too many sockets. The connections are reused and automatically disposed to avoid DNS level issues.
If you are interested in learning more about how it works internally checkout out this link here.
Consumption Patterns
There are different ways we can use IHttpClientFactory in our application code.
Basic Usage
The above usage of IHttpClientFactory is referred to as Basic usage, by directly injecting the factory instance into the Controller or class that requires an HttpClient instance. It works perfectly fine.
However, often when we need to make connections to external services, we also need a set of associated configuration details like URL, secret keys, special request headers, etc. While you can inject the configuration setting and other information into the Controller, the container soon starts violating the Single Responsibility Principle (SRP).
Named Clients
When using Named clients, the HttpClient instance configurations can be specified while registering the service with the Dependency Injection container. Instead of just calling the services.AddHttpClient()
method in Startup.cs,
we can add a client with a name and associated configuration.
Below we have a client with the name 'weather,' and it also configures the BaseAddress to use for the client.
services.AddHttpClient("weather", c =>
{
c.BaseAddress = new Uri("http://api.weatherapi.com/v1/current.json");
})
In the Controller class, when we need to create a new HttpClient, we can use the name to create a specific client.
var httpClient = _httpClientFactory.CreateClient("weather");
Typed clients
In the above code, we still need to hardcode the 'weather' string in the Controller and manually create a HttpClient ourselves.
To avoid calling the CreateClient
method explicitly, we can use the Typed client pattern.
The external API calls are refactored into a separate class (WeatherService
) for this pattern. This new class takes a dependency on the HttpClient directly, as shown below.
public interface IWeatherService
{
Task < string > Get(string cityName);
}
public class WeatherService: IWeatherService
{
private HttpClient _httpClient;
public WeatherService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task < string > Get(string cityName)
{
string APIURL = $ "?key={API_KEY}&q={cityName}";
var response = await _httpClient.GetAsync(APIURL);
return await response.Content.ReadAsStringAsync();
}
}
When adding the new class, WeatherService
to the Dependency Injection container, we can apply the relevant configuration, as shown below.
services.AddHttpClient<IWeatherService,WeatherService>(c => {
c.BaseAddress = new Uri("http://api.weatherapi.com/v1/current.json");
})
The Controller class can now use the WeatherService
and call it to get back the relevant data, as shown below.
public WeatherForecastController(IWeatherService weatherService)
{
_weatherService = weatherService;
}
[HttpGet]
public async Task<string> Get(string cityName)
{
return await _weatherService.Get(cityName);
}
By using IHttpClientFactory, we can solve all the initial issues that we saw with instantiating the HttpClient instance directly. After refactoring it to the Typed client consumption pattern, it is also well separated and easier to maintain. It drives us to write cleaner, loosely coupled code.
Are you using HttpClient the right way? Yes, you are now.
References:
Top comments (0)