In this tutorial, we are going to discuss the below topics
- How to identify the issues with HttpClient in .NET Core.
- How to use the HttpClient in the right way
The tools which I have used for this tutorial are below
- Visual Studio Community Edition 2022
- .NET 6.0
The source code can be found on GitHub
How to identify the issues with HttpClient in .NET Core
To begin with, I’m going to create a Web API application with the default WeatherForcast template and a Console Application which is going to consume the API.
The project structure as below
Let me brief the project structures
- HttpClientService - Web API which return the weather forecast
- HttpClientConsumerService - it’s another Web API that will consume the HttpClientService Web API
Please add the below lines of Code in HttpClientTestController.cs file in HttpClientConsumerService
namespace HttpClientConsumerService.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class HttpClientTestController : ControllerBase
{
public HttpClientTestController() { }
[HttpGet]
public async Task<string> TestHttpClient()
{
var url = "http://localhost:5014/WeatherForecast";
var httpClient= new HttpClient();
var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
}
}
Before executing the Project, apply the following settings
- Right-click on the Solution "HttpClientUsage" and click on "Properties"
- Select "Multiple startup Projects"
- Set the Action as "Start" for both projects
Now let us go ahead and execute the HttpClientConsumerService
We have got the result. Execute the same service multiple times.
So far so good. Now time to see what is happening behind the scenes with the HttpClient execution.
Let us utilize a popular command-line utility called netstat. This tool helps us to look at network statistics.
Let us go ahead and run the below command
netstat -na | find "5014" - here 5014 is the port, depends on your environment, the port will be changed.
For each request a socket gets opened with status "ESTABLISHED". This is not what we want.
If you go back and look at the code- you will come to know that we are not disposing the HttpClient.
Let us modify the code as below in HttpClientTestController
[HttpGet]
public async Task<string> TestHttpClient()
{
var url = "http://localhost:5014/WeatherForecast";
using (var httpClient = new HttpClient())
{
var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
}
Let us go ahead and execute the HttpClientTest multiple times and check the network statistics
Here we can see the status "TIME_WAIT" which means we have closed connection from our side, but we have a connection still open to see whether there are any packets that are delayed over the network. This is going to get disposed of after a default timeout period. However, for every request that we are making, HttpClient is creating an underlying socket. In this case, we are going to run out of sockets if we have so many requests. This issue is usually referred to as Socket Exhaustion problem.
Now let us see how this can be resolved using the Singleton pattern.
Time to make the necessary changes in HttpClientTestController.cs to make it as Singleton HttpClient instance.
public class HttpClientTestController : ControllerBase
{
private static HttpClient _httpClient;
static HttpClientTestController() {
_httpClient = new HttpClient();
}
[HttpGet]
public async Task<string> TestHttpClient()
{
var url = "http://localhost:5014/WeatherForecast";
var response = await _httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
}
Note: there are many ways to implement Singleton pattern. Here we have opted the static instance approach.
Now execute the service and check the net stat
You can see that one socket has been opened with “ESTABLISHED”. Now let us make a couple of requests and see whether it is creating a new socket or not.
There is no new instance created – the same connection has been used. This is because we are using singleton instance which opens a connection and maintain the scope through the application’s life cycle.
From the outside, it might see that, this resolved the socket exhaustion issue. However, we note that we have an open connection with status “ESTABLISHED”. In any case, if there are DNS changes or network related changes that can affect the connection, this application is going to fail. We will have to restart the application, so that it will create a new instance of HttpClient. This is not what we are looking for.
Let us see how this can be fixed!
.NET Framework has built-in support for creating the HttpClient instances. This can be achieved by using IHttpClientFactory interface.
Let's see the updated code below
public class HttpClientTestController : ControllerBase
{
private readonly IHttpClientFactory _httpClientFactory;
public HttpClientTestController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
[HttpGet]
public async Task<string> TestHttpClient()
{
var url = "http://localhost:5014/WeatherForecast";
var httpClient = _httpClientFactory.CreateClient();
var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
}
To register the IHttpClientFactory, in Program.cs file, add the below line of code
builder.Services.AddHttpClient();
Let us go ahead and execute the application and see whether it is working fine.
A new instance has been created. Let us go ahead and create a couple more requests and see
The same instance has been reused.
By using the _httpClientFactory.CreateClient() method, we are reusing the connections.
To understand more about IHttpClientFactory interface, requesting you to go through the Microsoft Documentation. This tutorial will help you to understand the issues with the original HttpClient class .NET framework and the benefits of using IHttpClientFactory.
There are multiple ways to consume IHttpClientFactory. I will be explaining this in my upcoming tutorial.