ASP.NET Core: Using HTTPCLIENT with a Correct Way

If you are building .NET Applications, then certainly your application needs to perform external API calls using HTTP Protocol.  Perhaps, if I say that the HTTP calls is a data fetching backbone of your applications then I won't be wrong. In such scenarios to perform HTTP calls we have the HttpClient class in .NET. The HttpClient class offers a great abstraction over managing HTTP Communications especially when the data communication is in the form of the JSON. The Figure 1 explains the scenario of the HTTP API calls.



Figure 1: The HttpClient to Access HTTP APIs 

But here are some possibilities where the HttpClient may be misused:

  1. If the new HttpClient instance is created every time, then that may result into port exhaustion. 
  2. Since the HttpClient implements IDisposable interface, we can use the HttpClient in the using statement for disposing its instance, but the problem is even after the HttpClient instance is disposed the underlying socker is not immediately release and this situation causes the socket exhaustion.
  3. The default constructor of HttpClient creates a concrete instance of  HttpMessageHandler class. This is a real reason behind the Socker exhaustion. Thats why this object is expensive.       
  4. The use of HttpClient client has the DNS behavior influenced by the underline SocketHandler.  The HttpClient only resolve the DNS entries when a connection is created. This means that each time when a new instance of the HttpClient is created the DNS entry will be resolved.     
So here the question is how we can use the HttpClient with the best possible way?  From the above discussed points, we can conclude that instead of creating new HttpClient instance every time, better we should reuse it. This means that the code shown in the Listing 1 must be avoided.


    [ApiController]
    [Route("api/[controller]")]
    public class MySampleController : ControllerBase
    {
        [HttpGet("getone")]
        public async Task<IActionResult> GetOne()
        {
            using (HttpClient client = new HttpClient())
            {
                HttpResponseMessage response = await client.GetAsync
                                   ("https://api.myapiserv.com/dataone");
              ...............
                return Ok(responseData);
            }
        }

        [HttpGet("gettwo")]
        public async Task<IActionResult> GetTow()
        {
            using (HttpClient client = new HttpClient())
            {
                HttpResponseMessage response = await client.GetAsync
                     ("https://api.myapiserv.com/datatwo");
                ...............
                return Ok(responseData);
            }
        }
    }
Listing 1: Each method defines an instance of HttpClient

As discussed above, the code in Listing 1 will result into the port exhaustion because the HttpClient is re-instantiated each time. If the instance of the HttpClient is reused for multiple requests, then it will reuse the cached DNS resolution for the initial connection. Here, the question is then how the HttpClient instance is created, one of the mechanisms is use the IHttpClientFactory contract. We can use this contract to instantiate the HttpClient class. This contract has following benefits:
  1. It offers a central location for naming and configuring logical HttpClient Objects.
  2. Manage the microservices resiliency, CircuitBreaker, etc.
  3. Manages the lifetime of the HttpMessageHandler that manages messaging.      
The IHttpClientFactory contract can be used using the ASP.NET Core Dependency Container as shown in Listing 2


// register the IHttpClientFactory in DI

 services.AddHttpClient();
 services.AddControllers();
 
 
 // Use the IHtpClientFactory in the Controller using the Dependency Injection
  [ApiController]
  [Route("api/[controller]")]
  public class MySampleController : ControllerBase
  {
     private readonly IHttpClientFactory _httpClientFactory;

     public MySampleController(IHttpClientFactory httpClientFactory)
     {
        _httpClientFactory = httpClientFactory;
     }

     [HttpGet("getone")]
     public async Task<IActionResult> GetOne()
     {
        var client = _httpClientFactory.CreateClient();
        HttpResponseMessage response = await client.GetAsync("https://api.myapiserv.com/dataone");
        ................
        return Ok(responseData);
     }

     HttpGet("gettwo")]
     public async Task<IActionResult> GetTwo()
     {
         var client = _httpClientFactory.CreateClient();
         HttpResponseMessage response = await client.GetAsync("https://api.myapiserv.com/datatwo");
               ................
                return Ok(responseData);
     }
        
  }
Listing 2: The user of IHttpClientFactory

Code in Listing 2 shows that the IHttpClientFactory is constructor injected in the controller class, (we can create a separate service class inject the IHttpClientFactory in it). The point here is that why is this better approach? Since the reusable HttpClient instance created using this contract has the HttpMessageHandler class instantiated, the internal HTTP Connection pool created by the HttpMessageHandler is reused for HTTP messaging. One more reason is that the IHttpClientFactory cache the HttpMessageHandler and reuse it when creating a new HttpClient instance.  

Handling the Duplicate Code Reduction with IHttpClientFactory with Named Client

Although IHttpClientFactory resolves all discussed issues causes because of HttpClient manual instantiation, we still need to do lot much work while obtaining the HttpClient instance created using the CreateClient() method. E.g. setting Header values, etc. This increases the code duplication each time when using the CreateClient() method. To resolved this, we can create Named Clients. In Listing 3 the AddHttpClient() method code modification is shown.


.......
builder.Services.AddHttpClient("MyServ",(serviceProvider, httpClient) =>
{
    var customerSettings = serviceProvider.GetRequiredService<IOptions<CustomerSettings>>().Value;
    httpClient.DefaultRequestHeaders.Add("Authorization", customerSettings .AuthObject);
    httpClient.BaseAddress = new Uri("https://api.myapiserv.com");
})
........
Listing 3: The Named client
      
As shown in Listing 3, we are directly defining the required HTTP headers in the created HttpClient instance. Also, the MyServ, the named we have passed to AddHttpClient() method to extract the HttpClient instance based on the name as shown in Listing 4.


public class MyService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public MyHttpClientService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<Data?> GetDataAsync()
    {
        var client = _httpClientFactory.CreateClient("MyServ");

        Data? data = await client
            .GetFromJsonAsync<Data>("/dataone");

        return data;
    }
}
Listing 4: The Named Client for HttpClient
 
 We need to register the MyService in the ASP.NET Core DI. Certainly, the approach used in the Named Client looks great but if you have noticed that to resolve the HttpClient we need to pass the MyServ each time. To avoid this, it's better to use the better approach of Typed Clients.

Using the Typed Clients
 This is one of the best mechanisms of using HttpClient in ASP.NET Core. In this case instead of using the name to resolve HttpClient, we use the Service class and pass it as a typed parameter to AddHttpClient() method. One important point here is that the Service class which manage HTTP calls must be registered as Transient-Lifetime in the ASP.NET Core dependency container. To demonstrate the Typed Client approach, I have used the ASP.NET Core API that is using the API-Key Authentication. I have already written an article which can be read from this link

Let's implement the application code. Download the code for ASP.NET Core API Authentication project from the Git Repo. Download the code and run it. You can change the port to 7001 to run the API. 

Step 1: Open Visual Studio 2022 and create a new ASP.NET Core API project named 'Core_HttpClientAPI'. 
Step 2: In this project, add a new folder named Settings. In this folder, add a new C# class named `CustomerSettings. In this class add the code shown in Listing 5 to define AuthKey and AuthHeader properties. Thes properties will be used in the HTTP Header to make the HTTP Class to external HTTP API.


public class CustomerSettings
{
    public const string ConfigurationSection = "Customer";
    public string AuthKey { get; set; } = "d3vnwDLqBCEcNkLY0uIwYg5dzyIAcMtSdwt7DuzdLc=";
    public string AuthHeader { get; set; } = "X-API-AuthKey";
}
Listing 5: The CustomerSettings class
 
Step 3: Since the external HTTP API returns the Customer data, we need to create Customer class in the current Project in tee Models folder by adding the Customer.cs class file as shown in Listing 6.


public partial class Customer
{
    public string CustomerId { get; set; } = null!;

    public string CustomerName { get; set; } = null!;

    public string Address { get; set; } = null!;

    public string City { get; set; } = null!;
}
Listing 6: The Customer class
               
Step 4: In the project, add a new folder named Services. In this folder, add a class file named CustomerService.cs. This service class will be our Typed Client. In this class, we will inject the HttpClient using the constructor injection and hence we are free from using the IHttpClientFactory contract as well as an explicit instance creation of the HttpClient class. The HttpClient will be resolved using the AddHttpClient() service method. The code in Listing 7 shows an implementation of the CustomerService class.


using Core_HttpClientAPI.Models;
using System.Text.Json;

namespace Core_HttpClientAPI.Services
{
    public class CustomerService
    {
        private readonly HttpClient _httpClient;
        public CustomerService(HttpClient client)
        {
            _httpClient = client;
        }

        public async Task<List<Customer>> GetAsync()
        {
            var customers = await _httpClient.GetFromJsonAsync<List<Customer>>("customers");
            return customers;
        }

        public async Task<Customer> PostAsync(Customer cust)
        {
            var content = await _httpClient.PostAsJsonAsync<Customer>("customers", cust);

            var responseContent = await content.Content.ReadAsStringAsync();
            var options = new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            };
            var customer = JsonSerializer.Deserialize<Customer>(responseContent, options);
            return customer;
        }
    }
}
Listing 7: The CustomerService class

The Listing 7 shows the Get and Post operations performed using the HttpClient class that is injected in the CustomerService class. So, no more named client parameter. Now the further challenge here is to register the CustomerService class. You must register the Typed Client as Transient Lifetime. This will handle DNS Resolution and Port exhaustion. 

Step 5: Modify the code in Program.cs to register the CustomerService class as Transient Lifetime and also use it as a Types Client as shown in Listing 8.


// The Settings class to read the Authentication infromation
builder.Services.AddOptions<CustomerSettings>()
    .BindConfiguration(CustomerSettings.ConfigurationSection);
builder.Services.AddTransient<CustomerService>();

builder.Services.AddHttpClient<CustomerService>((serviceProvider, httpClient) =>
{
    var customerSettings = serviceProvider.GetRequiredService<IOptions<CustomerSettings>>().Value;
    httpClient.DefaultRequestHeaders.Add(customerSettings.AuthHeader, customerSettings.AuthKey);
    httpClient.BaseAddress = new Uri("http://localhost:7001");
});
Listing 8: The Program.cs Modification

The code in Listing 8 shows the CustomerSetrvice class used as a Typed Parameter to AddHttpClient() method and hence it is a Typed Client. The HTTP headers are set using the AuthKey and AuthHeader values from the CustomerSettings class.

Step 6: Let's create Endpoints for Get and Post requests. In the project, add a folder named AppEndpoints. In this folder, add a new class file named CustomerEndpoints.cs. In this class file we will add a class named CustomerEndpoints that will create an extension method for IEndpointRouteBuilder. The name of the extension method will be MapCustomerEndpoints(). In this method we will add code for MapGet() and MapPost() methods. Both these methods will use the CustomerService injection to access methods from the CustomerService class to perform Get and Post operations. The code for CustomerEndpoints class is shown in Listing 9.


 public static class CustomerEndpoints
 {
     public static void MapCustomerEndpoints(this IEndpointRouteBuilder app)
     { 
         app.MapGet("/customer", async(CustomerService serv) => 
         {
             var result = await  serv.GetAsync();
             return Results.Ok(result);
         });

         app.MapPost("/customer", async (CustomerService serv, Customer cust) => {
           var result = await   serv.PostAsync(cust);
           return Results.Ok(cust);
         });
     }
 }
Listing 9: The CustomerEndpoints class       

Let's call the MapCustomerEndpoints() method in Program.cs as shown in Listing 10.


.......
app.MapCustomerEndpoints();
.......
Listing 10: The accessing MapCustomerEndpoints() method 
 
That's it.

Run the downloaded API project also run the Core_HttpClientAPI project. Either use the Postman or Advanced REST Client to test this API by adding Custom HTTP headers as shown in the Figure 2



Figure 2: The App testing

So, we can perform GET and POST operations. The code for this article can be downloaded from this link

Conclusion:  The HttpClient is the best object to make HTTP calls from .NET applications to external HTTP APIs. To keep in mind that we should not create an instance of HttpClient but instead use the Typed Client to resolve an instance of HttpClient class. This will help to handle the port exhaustion.    
             

Popular posts from this blog

Uploading Excel File to ASP.NET Core 6 application to save data from Excel to SQL Server Database

ASP.NET Core 6: Downloading Files from the Server

ASP.NET Core 6: Using Entity Framework Core with Oracle Database with Code-First Approach