It is common for APIs to use HTTP requests to communicate with other APIs.
HTTP requests include all necessary details sent to the server, such as URL, headers, parameters and more.
Once the server receives the request, it processes it and sends back a response. This response contains a status code and also may include resource data.
In the .NET ecosystem, you can use HttpClient to easily send HTTP requests.
HttpClient
HttpClient is a class used to send HTTP requests and receive HTTP responses from resources identified by a URI.
HttpContent type represents the HTTP entity body and its associated content headers.
When a valid response is received, you can access the response body through the Content property. This allows you to retrieve the body as a stream, byte array or string.
If an HTTP endpoint returns JSON, the response body can be easily deserialized into a C# object using the System.Net.Http.Json NuGet package.
This package provides several extension methods for HttpClient and HttpContent, enabling automatic serialization and deserialization of data.
var client = new HttpClient();
client.DefaultRequestHeaders.Add("ApiKey", _settings.ApiKey);
client.BaseAddress = new Uri(_settings.BaseAddress);
var userInfo = await client
.GetFromJsonAsync<UserInfo>($"users/{userId}");
However, it's not all sunshine and roses, improper use of HttpClient can lead to several potential issues.
HttpClient in .NET is designed to be reused for multiple HTTP requests.
If you create a new HttpClient instance for every request, you can run into port exhaustion because each new instance creates its own set of TCP connections.
Potential issues when using HttpClient.IHttpClientFactory
IHttpClientFactory solves many issues easily. It basically manages the creation and lifecycle of HttpClient instances and their underlying handlers.
It reuses the underlying HttpMessageHandler instances, which prevents port exhaustion.
app.MapGet("client-factory", async (IHttpClientFactory factory, CancellationToken cancellationToken) =>
{
using var client = factory.CreateClient();
client.BaseAddress = new Uri("https://cex.io/api/");
var response = await client.GetFromJsonAsync<CurrencyLimitResponse>(
"currency_limits",
cancellationToken);
return Results.Ok(response);
});
However, this approach can become cumbersome when you need to define headers and URIs for each HTTP request.
To reduce code duplication, here are two common approaches:
- Named Clients
- Typed Clients
Named Clients
The easiest solution is to use named clients. Named Clients, a feature of IHttpClientFactory, allows you to create and configure multiple HttpClient instances with unique settings.
Each client is assigned a unique name, allowing you to retrieve the specific client by its name when needed.
First, you must register the named client using the AddHttpClient method and configure it with a name and specific settings:
builder.Services.AddHttpClient("currency", client =>
client.BaseAddress = new Uri("https://cex.io/api/"));
After registering it, you can retrieve an instance of the named client:
app.MapGet("named-client", async (IHttpClientFactory factory, CancellationToken cancellationToken) =>
{
using var client = factory.CreateClient("currency");
var response = await client.GetFromJsonAsync<CurrencyLimitResponse>(
"currency_limits",
cancellationToken);
return Results.Ok(response);
});
This solution provides an easy, centralized configuration while avoiding issues commonly associated with HttpClient.
However, with named clients, you still need to manage the request logic, which can become scattered throughout your project.
Typed Clients
To centralize everything, you can create typed clients. Typed Clients encapsulate the logic for interacting with a specific API within a strongly-typed class.
To create a typed client, you first need to define a class that handles the logic for interacting with an API.
public class CryptoApiClient(HttpClient client)
{
public async Task<CurrencyLimitResponse?> GetLimits(CancellationToken cancellationToken = default) =>
await client.GetFromJsonAsync<CurrencyLimitResponse>(
"currency_limits",
cancellationToken);
}
It should inject the HttpClient into the constructor.
Next, you need to register it in DI using the AddHttpClient method:
builder.Services.AddHttpClient<CryptoApiClient>(client =>
client.BaseAddress = new Uri("https://cex.io/api/"));
Fun fact: Behind the scenes, typed clients are essentially named clients, utilizing the same underlying mechanisms. When you register a Typed Client with AddHttpClient<TClient>, it is registered as a Named Client, but with added functionality to instantiate the Typed Client class and inject the HttpClient into it.
Once the typed client is registered, you can inject it and use it:
app.MapGet("typed-client", async (CryptoApiClient cryptoApi, CancellationToken cancellationToken) =>
{
var response = await cryptoApi.GetLimits(cancellationToken);
return Results.Ok(response);
});
I personally prefer strongly typed clients because they centralize the logic, and I find them easier to work with and mock for creating tests as well.
Conclusion
HttpClient is a powerful tool, but it can be easily mismanaged, leading to potential issues.
IHttpClientFactory is an effective solution because of its handling of the lifecycles of HttpClient instances.
Both named and typed clients help centralize configuration and reduce code duplication, with typed clients offering a more structured and maintainable approach.
In future blog posts, we'll dive into strategies for making HTTP clients more resilient and explore third-party libraries to achieve similar results.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!