HTTP Best Practices Using ASP.NET Core

Past few years, we are using Httpclient but are we using it properly? 

Recently, I came across the code of a senior developer from the other team. The developer was trying to communicate with the microservices by using HttpClient and has implemented his own logic of retry. 

Implementing the own logic for retry is not wrong but understanding what overhead it could bring is more important. 

The logic that he has written has some major drawbacks 

  • Usage of HttpClient  
  • Usage of retry using loops

In this article, we’ll be covering only about drawbacks of HttpClient and how to fix the issues related to it. 

Please find the source code here.

Usage of HttpClient

HttpClient is used to send the HTTP request or receive the HTTP response from the desired endpoint specified. 

Internally, HttpClient uses HttpMessageInvoker class.  HttpMessageInvoker class allows the applications to call the SendAsync method(send the HTTP request as an async operation). However, HttpMessageInvoker uses an IDisposable interface to release unmanaged resources. 

//
  // Summary:
  //     Provides a class for sending HTTP requests and receiving HTTP responses from
  //     a resource identified by a URI.
  public class HttpClient : HttpMessageInvoker
  {
      //
      // Summary:
      //     Gets or sets the base address of Uniform Resource Identifier (URI) of the Internet
      //     resource used when sending requests.
      //
      // Returns:
      //     The base address of Uniform Resource Identifier (URI) of the Internet resource
      //     used when sending requests.
      public Uri? BaseAddress
      {
          [System.Runtime.CompilerServices.NullableContext(2)]
          get
          {
              throw null;
          }
          [System.Runtime.CompilerServices.NullableContext(2)]
          set
          {
          }
      }
 
      //
      // Summary:
      //     Gets or sets the global Http proxy.
      //
      // Returns:
      //     A proxy used by every call that instantiates a System.Net.HttpWebRequest.
      //
      // Exceptions:
      //   T:System.ArgumentNullException:
      //     The value passed cannot be null.
      public static IWebProxy DefaultProxy
      {
          get
          {
              throw null;
          }
          set
          {
          }
      }

//
// Summary:
//     A specialty class that allows applications to call the System.Net.Http.HttpMessageInvoker.SendAsync(System.Net.Http.HttpRequestMessage,System.Threading.CancellationToken)
//     method on an HTTP handler chain.
public class HttpMessageInvoker : IDisposable
{
    //
    // Summary:
    //     Initializes an instance of a System.Net.Http.HttpMessageInvoker class with a
    //     specific System.Net.Http.HttpMessageHandler.
    //
    // Parameters:
    //   handler:
    //     The System.Net.Http.HttpMessageHandler responsible for processing the HTTP response
    //     messages.
    public HttpMessageInvoker(HttpMessageHandler handler)
    {
    }
 
    //
    // Summary:
    //     Initializes an instance of a System.Net.Http.HttpMessageInvoker class with a
    //     specific System.Net.Http.HttpMessageHandler.
    //
    // Parameters:
    //   handler:
    //     The System.Net.Http.HttpMessageHandler responsible for processing the HTTP response
    //     messages.
    //
    //   disposeHandler:
    //     true if the inner handler should be disposed of by Dispose(), false if you intend
    //     to reuse the inner handler.
    public HttpMessageInvoker(HttpMessageHandler handler, bool disposeHandler)
    {
    }
 
    //
    // Summary:
    //     Releases the unmanaged resources and disposes of the managed resources used by
    //     the System.Net.Http.HttpMessageInvoker.
    public void Dispose()
    {
    }
 
    //
    // Summary:
    //     Releases the unmanaged resources used by the System.Net.Http.HttpMessageInvoker
    //     and optionally disposes of the managed resources.
    //
    // Parameters:
    //   disposing:
    //     true to release both managed and unmanaged resources; false to releases only
    //     unmanaged resources.
    protected virtual void Dispose(bool disposing)
    {
    }
 
    //
    // Summary:
    //     Sends an HTTP request with the specified request and cancellation token.
    //
    // Parameters:
    //   request:
    //     The HTTP request message to send.
    //
    //   cancellationToken:
    //     The cancellation token to cancel operation.
    //
    // Returns:
    //     The HTTP response message.
    //
    // Exceptions:
    //   T:System.ArgumentNullException:
    //     The request was null.
    //
    //   T:System.NotSupportedException:
    //     For HTTP/2 and higher or when requesting version upgrade is enabled by System.Net.Http.HttpVersionPolicy.RequestVersionOrHigher.
    //     -or- If using custom class derived from System.Net.Http.HttpContent not overriding
    //     System.Net.Http.HttpContent.SerializeToStream(System.IO.Stream,System.Net.TransportContext,System.Threading.CancellationToken)
    //     method. -or- If using custom System.Net.Http.HttpMessageHandler not overriding
    //     System.Net.Http.HttpMessageHandler.Send(System.Net.Http.HttpRequestMessage,System.Threading.CancellationToken)
    //     method.
    [UnsupportedOSPlatform("browser")]
    public virtual HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        throw null;
    }
 
    //
    // Summary:
    //     Send an HTTP request as an asynchronous operation.
    //
    // Parameters:
    //   request:
    //     The HTTP request message to send.
    //
    //   cancellationToken:
    //     The cancellation token to cancel operation.
    //
    // Returns:
    //     The task object representing the asynchronous operation.
    //
    // Exceptions:
    //   T:System.ArgumentNullException:
    //     The request was null.
    public virtual Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        throw null;
    }
}

Current Implementation

I’m using the default Web API template for both frontend and backend applications.

Backend

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

You might have noticed, it’s a default weather controller that comes with a Web API template.

Frontend

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;
    [HttpGet(Name = "GetWeatherForecast")]
    public async Task Get()
    {
        for (int i = 0; i < 100; i++)
        {
            var httpClient = new HttpClient();
            var response = await httpClient.GetAsync("https://localhost:7054/WeatherForecast").Result.Content.ReadAsStringAsync();
        }
    }
}

I’m iterating the HTTPClient 100 times and sending the request to the backend application. All works and everything is right.

But wait, there’s more! 

Let’s use the netstat tool to understand the state of the socket on the machine running

HTTP best practices using ASP.NET Core

Huh, that’s weird. The application has exited but still, there are a bunch of connections open. They are in the time_wait state which means the connection has been closed on one side(ours) but still waiting for the additional packets coming in on it because of the network delay. 

As discussed before, HttpClient internally uses an IDisposable interface. Try the Using keyword to release the socket after completing our operation. 

public async Task Get() {
    for (int i = 0; i < 100; i++) {
        using
        var httpClient = new HttpClient();
        var response = await httpClient.GetAsync("https://localhost:7054/WeatherForecast").Result.Content.ReadAsStringAsync();
    }
}

Now, let us perform netstat again,

HTTP best practices using ASP.NET Core

Even after applying Using keyword, the results are the same then what is the use of Disposable interface – not able to release the sockets even after completion.  
 
When dispose of is called, the connection will stay open for 4 mins to handle the network traffic. 
 
Unlike Monolithic architecture, usage of HttpClient has increased in the new era of microservice architecture. Microservice or container-based applications ideally have limited resources. Ex: 400 MB of Memory and  400 MI of CPU

Now imagine, the socket remaining open for 4 mins with the container of limited resources. The sockets will exhaust soon and the application starts throwing a socket exhaustion error. 

Enough about drawbacks, solution time

HttpClientFactory has been introduced in .Net Core 2.1 and it’s been around for quite a while.

HttpClientFactory manages the life cycle of the HTTP instance. All the life cycle management is taken care of by the factory class and provides a nice way of configuring the HTTP client in the startup. You have the option to implement factory class either by using Named clients or typed clients. 

Named Client

Named clients are useful when,

  • App uses HttpClient for different components/services
  • Many HttpClient instances have different configurations. Ex: One HttpClient instance for Order service and another instance for Product service.
public class WeatherForecastController : ControllerBase
{
    private readonly ILogger<WeatherForecastController> _logger;
    private readonly IHttpClientFactory _httpClientFactory;

    public WeatherForecastController(ILogger<WeatherForecastController> logger,IHttpClientFactory httpClientFactory)
    {
        _logger = logger;
        _httpClientFactory = httpClientFactory;
       }
    #endregion

    [HttpGet(Name = "GetWeatherForecast")]
    public async Task Get()
    {
        for (int i = 0; i < 100; i++)
        {               
            using var httpClient = _httpClientFactory.CreateClient("weatherForecast");
            string url = $"{httpClient.BaseAddress}WeatherForecast";
            var response = await httpClient.GetAsync(url).Result.Content.ReadAsStringAsync();
            _logger.LogInformation(response);
        }
    }
}

Changes in Program.cs,

builder.Services.AddHttpClient("weatherForecast", client =>
{
   client.BaseAddress = new Uri("https://localhost:7054");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

AddHttpClient method is used to register the HTTP instance for HttpClientFactory. The parameter/name to invoke the HTTP instance of the weather forecast class is “weatherForecast”.

Let us perform a similar test using netstat again,

HTTP best practices using ASP.NET Core

Unlike HttpClient, a single socket is created with the status “Established” and the lifetime of the socket will be completed and managed by HttpClientFactory.

Typed Client

Typed clients are better than Named clients

  • Typed clients are strongly typed and no need to inject IHttpClientFactory
  • Typed clients can be injected directly into your classes.

Create a service layer to properly utilize the typed client

public interface IWeatherService
{
    Task<string> GetWeatherAsync();
}
public class WeatherService : IWeatherService
{
    private readonly ILogger<WeatherService> logger;
    private readonly HttpClient httpClient;

    public WeatherService(ILogger<WeatherService> logger,HttpClient httpClient)
    {
        this.logger = logger;
        this.httpClient = httpClient;
    }

    public async Task<string> GetWeatherAsync()
    {
        string url = $"{httpClient.BaseAddress}WeatherForecast";
        var response = await httpClient.GetAsync(url).Result.Content.ReadAsStringAsync();
        logger.LogInformation(response);
        return response;
    }
}

Changes in the program.cs

builder.Services.AddHttpClient<IWeatherService,WeatherService>(client =>
{
    client.BaseAddress = new Uri("https://localhost:7054");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
});

Changes in the WeatherForecast Controller

[ApiController]
[Route("[controller]")]
public class WeatherForecastController: ControllerBase {
    private readonly ILogger < WeatherForecastController > _logger;
    private readonly IWeatherService weatherService;
    public WeatherForecastController(ILogger < WeatherForecastController > logger, IWeatherService weatherService) {
            _logger = logger;
            this.weatherService = weatherService;
        }
        [HttpGet(Name = "GetWeatherForecast")]
    public async Task Get() {
        for (int i = 0; i < 100; i++) {
            var response = await weatherService.GetWeatherAsync();
            _logger.LogInformation(response);
        }
    }
}

Let us perform a similar test using netstat again

HTTP best practices using ASP.NET Core

The result remains the same for Named and Typed Clients.

Like the Named client, a single socket is created with the status “Established” and the lifetime of the socket will be completed and managed by HttpClientFactory.

What’s next

In the next article, we’ll discuss resiliency for HTTP calls such as Retry and Circuit breakers using Polly.