Tutorials

Getting started with HttpClientFactory in C# and .NET 5

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, and typed.

Getting started with HttpClientFactory in C# and .NET 5

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:

Diagram showing how HTTPCLient passes the request and response through messagehandlers and the HTTPClientHandler to the network

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 handle HttpClient 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.