HttpClientFactory
has been around the .NET ecosystem for a few years now.
In this post we will look at 3 basic implementations of HttpClientFactory
:
- basic
- named
- typed
All the code in this post is available in this GitHub repository.
First, let's learn about what HttpClient
is, how HttpClientFactory
fits into the picture and why we would want to use it.
Why can’t I just use HttpClient
?
You can of course just use the HttpClient
, but let's look a little more closely at what HttpClient
is actually doing.
The following diagram from the Microsoft documentation shows the relationship between the client and the handlers:
The default handler, HttpClientHandler
actually sends the request over the network and gets the response from the server. Custom message handlers can be inserted into the client pipeline if required.
You may be familiar with an implementation of HttpClient
similar to the following in your web projects:
using (var httpClient = new HttpClient())
{
httpClient.BaseAddress = new Uri("https://api.twilio.com/2010-04-01/");
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var responseMessage = await httpClient
.GetAsync(apiEndPoint);
}
There is nothing inherently wrong with the above code when considered in isolation.
HttpClient
implements IDisposable
which sees many developers creating it within a using
block. Then, once out of scope, it will be properly disposed of. However, in the blog “You’re using HttpClient wrong and it’s destabilizing your software” on "ASP.NET Monsters'" you can see that this is not the ideal way to proceed.
Now because HttpClient
is generally thread safe. This may lead some developers to create the client as a singleton and is actually the recommendation in the HttpClient Documentation.
However, this comes with its own set of issues. For example, the client will keep connections open for the lifespan of the application, it won't respect the DNS TTL settings and it will never get DNS updates. So this isn't a perfect solution either.
What is the HttpClientFactory?
The primary and authoritative reference material is from the Microsoft Documentation.
This documentation describes HttpClientFactory
as being "an opinionated factory for creating HttpClient
instances to be used in your applications”.
HttpClientFactory
middleware was created partly to try to address some of the issues raised above.
HttpClientFactory Key Features
So what are some of the key features? Again drawing on the reference material we can learn that:
- It provides a central place to name and configure our
HttpClients
. - Delegates handlers in
HttpClient
and implements Polly-based middleware to take advantage of Polly’s policies for resiliency. - HTTP clients are registered in a factory
- Can use a Polly handler that allows Polly policies to be used for better resiliency
- It manages the lifetime of
HttpClientHandlers
to avoid the aforementioned issues with trying to handleHttpClient
lifetimes yourself
In this blog we'll be looking at several ways to implement the HttpClientFactory
using the AssemblyAI API:
- Use
HttpClientFactory
directly - Use named clients
- Use typed clients
Basic HttpClientFactory usage
A basic HttpClientFactory
can be instanced via Dependency Injection.
First we will need to add the following code to the Startup
class within the ConfigureServices
method:
// File: Startup.cs
public class Startup
{
// Code deleted for brevity.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
// Remaining code deleted for brevity.
}
// Code deleted for brevity.
}
Then, in the class in which you wish to make a REST call, you can request the HttpClientFactory
. We will show this in an API Controller:
[ApiController]
public class ExampleController : Controller
{
private readonly IHttpClientFactory _clientFactory;
public ExampleController(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
[HttpPost]
public async Task Basic()
{
var json = new
{
audio_url = "https://s3-us-west-2.amazonaws.com/blog.assemblyai.com/audio/8-7-2018-post/7510.mp3"
};
string jsonString = JsonSerializer.Serialize(json);
var payload = new StringContent(jsonString, Encoding.UTF8, "application/json");
var client = _clientFactory.CreateClient();
client.DefaultRequestHeaders.Add("Authorization", "YOUR_ASSEMBLY_AI_TOKEN");
HttpResponseMessage response = await client.PostAsync("https://api.assemblyai.com/v2/transcript", payload);
string responseJson = await response.Content.ReadAsStringAsync();
}
}
The client factory will handle the disposal of the HttpClient
created in the above code.
Named HttpClientFactory clients
The previous code enables you to define the HttpClient
at time of use. However, client configuration, such as an API endpoint URL, can be defined in one place within the Startup
class. The named client can then be requested via dependency injection making the code more reusable. This is great if you have many distinct uses of HttpClient
or if you have many clients with different configurations.
The code below shows how we name and define an HttpClient
within the Startup
class.
// File: Startup.cs
public class Startup
{
// Code deleted for brevity.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("AssemblyAIClient", client =>
{
client.BaseAddress = new Uri("https://api.assemblyai.com/v2/transcript");
client.DefaultRequestHeaders.Add("Authorization", "YOUR_ASSEMBLY_AI_TOKEN");
});
// Remaining code deleted for brevity.
}
// Code deleted for brevity.
}
The named HttpClient
is consumed in a similar manner as the basic method, only this time we need to request the named instance from the factory. We also use the SendAsync
API instead, so setting up the request is a little different.
[ApiController]
public class NamedController : Controller
{
private readonly IHttpClientFactory _clientFactory;
public NamedController(IHttpClientFactory clientFactory)
{
_clientFactory = clientFactory;
}
[HttpPost]
public async Task Post()
{
var json = new
{
audio_url = "https://s3-us-west-2.amazonaws.com/blog.assemblyai.com/audio/8-7-2018-post/7510.mp3"
};
string jsonString = JsonSerializer.Serialize(json);
var payload = new StringContent(jsonString, Encoding.UTF8, "application/json");
var client = _clientFactory.CreateClient("AssemblyAIClient");
var request = new HttpRequestMessage(HttpMethod.Post, string.Empty);
var response = await client.SendAsync(request);
string responseJson = await response.Content.ReadAsStringAsync();
}
}
Whenever we call _clientFactory.CreateClient("AssemblyAIClient");
a new client is created with all the predefined configuration, so there is no need to define it within the method making the request.
Typed HttpClientFactory clients
Typed clients are very similar to named clients, but rather than using a string as a key, we instead take advantage of strong types. This improves our code by avoiding the use of potentially brittle strings also means that intellisense and compiler support are available to help when creating clients.
A typed client is a great way to encapsulate all of the logic, therefore keeping the Startup
class cleaner and easier to read and maintain.
To create a typed client, we will need to first create a class that is used to encapsulate our logic. We will call it AssemblyAiService
in this instance.
public class AssemblyAiService
{
public HttpClient Client { get; }
public AssemblyAiService(HttpClient client)
{
client.BaseAddress = new Uri("https://api.assemblyai.com/");
client.DefaultRequestHeaders.Add("Authorization", "YOUR_ASSEMBLY_AI_TOKEN");
Client = client;
}
public async Task<string> UploadAudioFile(StringContent payload)
{
HttpResponseMessage response = await Client.PostAsync("v2/transcript", payload);
string responseJson = await response.Content.ReadAsStringAsync();
return responseJson;
}
}
In the above code, we have created a simple method that accepts StringContent
and then posts that to the AssemblyAI endpoint. The method returns the JSON string response.
We will need to configure the client in the Startup
class just as we have done in the previous examples.
// File: Startup.cs
public class Startup
{
// Code deleted for brevity.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<AssemblyAiService>();
// Remaining code deleted for brevity.
}
// Code deleted for brevity.
}
As you can see, it is now much easier to read and understand what service has been added when using a typed HttpClient
.
Consumption of the client in the controller is also nicely encapsulated, extracting a lot of the boilerplate code away into a service.
[ApiController]
public class TypedController : Controller
{
private readonly AssemblyAiService _assemblyAiService;
public TypedController(AssemblyAiService assemblyAiService)
{
_assemblyAiService = assemblyAiService;
}
[HttpPost]
public async Task Post()
{
var json = new
{
audio_url = "https://s3-us-west-2.amazonaws.com/blog.assemblyai.com/audio/8-7-2018-post/7510.mp3"
};
string jsonString = JsonSerializer.Serialize(json);
var payload = new StringContent(jsonString, Encoding.UTF8, "application/json");
string responseJson = await _assemblyAiService.UploadAudioFile(payload);
}
}
The above code is also much more testable as it follows the SOLID principles more closely.
Conclusion
I hope the code above has inspired you to try out HttpClientFactory
in your next project.
There are a lot more benefits to HttpClientFactories
such as Policy Management with policies from the Polly Register and combining third-party libraries into generated clients.