The .NET framework comes with a suite of tools to build distributed systems. A distributed system involves components in separate networked computers. These systems can have a good amount of network chatter over the HTTP protocol. The HttpClient class, for example, is one tool that is useful for making calls into an HTTP endpoint.
In a distributed system with many service endpoints, one strategy can be building microservices around a domain boundary. This strategy often means reducing direct calls to a database and calling an endpoint to read or update data. Microservices increase network chatter because you may call more than one endpoint to work with all the data you need.
In this article, I’ll cover some tactics for using the HttpClient class to make calls into a service endpoint. I will go over pitfalls and implications when you use this class. I will focus on general tactics to make better use out of this tool that is part of the standard .NET framework.
A tactic differs from a strategy in that a strategy is a high-level plan. Creating microservices, for example, is a strategy in how you deal with data in a solution. A tactic, however, is more about implementation details. An implementation often includes code samples and best practices for programmers.
I will go over four tactics on how to make use of the HttpClient class. The first three will have a few issues which are of interest to anyone working with this class. Think of each tactic as making incremental improvements as you learn more about this class. (Sample projects are included at the bottom of the article.)
The HttpClient class has been with us since .NET 4.5. In fact, it is part of the .NET Standard 1.1. The .NET 4.5 framework version matches up with the 1.1 release of the standard. There are a few tweaks like new methods in subsequent versions of the standard but the behavior of the class itself remains the same.
To begin, let’s go over a naïve use of the HttpClient class.
A Naïve Approach
Imagine you have an endpoint you want to read data from using a GET verb. To simulate load, the code will iterate through this call ten times. At each iteration, it calls up the endpoint with a GET request and keeps going. This call will have to wait for a network response so it’s best to make it asynchronous, this way it doesn’t block any threads.
Because you’re working with the HttpClient class and async threading, you’ll need the following using statements:
1 2 |
using System.Net.Http; using System.Threading.Tasks; |
Keep in mind that all four tactics will use this same set of using statements, so I don’t have to repeat myself. One way to call up an endpoint using the HttpClient class is to do the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class NaiveApproach { public async Task SendRequestToEndpoint() { for (var count = 0; count < 10; count++) { using (var client = new HttpClient()) { await client.GetStringAsync("https://www.bing.com"); } } } } |
This code sample looks easy enough, but it has the following issues:
-
The class gets a new instance per method use which means more open sockets
-
The class gets glued to the container class which increases tight coupling
-
The class does dispose of itself but leaves sockets to timeout with only a single use
To test this, you can open the command prompt and run netstat -nP TCP on the console. Doing an nslookup www.bing.com shows the IP address as 204.79.197.200 which matches what is seen on the console.
For example:
As shown, every new instance of the HttpClient class opens a new socket with a new port number. Under load, a web server running this sample code will run out of sockets and start dropping connections. With a microservice strategy, for example, this will be a disaster because there are more services opening sockets per box.
TIME_WAIT indicates the HttpClient class does not close sockets immediately. Instead, it leaves sockets open in case any need to be reused. In computer programming, opening and closing network sockets is an expensive operation. This class abstraction knows this, and leaves sockets open for later use.
The aim of the HttpClient class is to reuse its open sockets before they time out. One way to do this is with a static singleton.
Using A Static Singleton
A typical static singleton definition will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class StaticSingleton { private static readonly HttpClient _httpClient = new HttpClient(); public async Task SendRequestToEndpoint() { for (var count = 0; count < 10; count++) { await _httpClient.GetStringAsync("https://www.bing.com"); } } } |
This solves the problem of opening too many sockets under load. The container class encapsulates the HttpClient class under a private static variable. The container class creates a singleton instance with a ‘has a’ relationship. This way of modeling classes is called object composition and it is a clean way to isolate concerns.
But, this way of solving the immediate problem is not free of issues. I can think of several items:
-
The static singleton’s new instance is not thread-safe
-
The static singleton is never properly disposed of
-
This singleton is only useful in this container class due to tight-coupling
If, for example, you want to create more classes that talk to some other endpoint, then, you may need to create a static singleton for each container class. This is a direct consequence of tight coupling the static singleton to the container class.
Given two threads that call up this class for the first time and at the same time, it is possible to end up creating two new instances of the static singleton. It is an edge case but one that should be considered. The goal of a singleton is to protect the instantiation process from creating multiple instances in an async context.
The HttpClient class is more suitable as a singleton for a single app domain. This means the singleton should be shared across multiple container classes. With this tactic, you do get a singleton, but this makes it difficult to share.
The HttpClient class implements the IDisposable interface. This class inherits from the HttpMessageInvoker class which implements the interface. In .NET, anything that implements this will need an explicit call to the IDisposable.Dispose method. The CLR can clean up items in managed code through garbage collection. But, when a class implements IDisposable, it means it accesses resources in native code. All programs that acquire resources such as a network socket, for example, must be released after use. In the .NET framework, this is done manually through the IDisposable interface.
This tactic is a step in the right direction but somewhat lacking. Now turn to inheritance to share the singleton.
Using Class Inheritance
To begin, because you will implement the IDisposable interface, be sure to add the following using statement:
1 |
using System; |
The parent class will do two things, implement IDisposable and contain the HttpClient singleton in a thread-safe way. The singleton pattern has a way to add thread safety using a lock block. The dispose pattern is the canonical way to clean up native resources after use.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public abstract class BaseClient : IDisposable { private static object _locker = new object(); private static volatile HttpClient _client; protected static HttpClient Client { get { if (_client == null) { lock (_locker) { if (_client == null) { _client = new HttpClient(); } } } return _client; } } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public virtual void Dispose(bool disposing) { if (disposing) { if (_client != null) { _client.Dispose(); } _client = null; } } } |
The parent class is an abstract class that will contain the HttpClient class and manage it. The singleton is declared as volatile to ensure that assignment to the static variable is complete before releasing the lock. The singleton pattern does a double-check locking approach to guarantee a lock on every call to the Client property.
To implement a service client from this base parent class, one can do:
1 2 3 4 5 6 7 8 9 10 |
public class BingImplementedClient : BaseClient { public async Task SendRequestToEndpoint() { for (var count = 0; count < 10; count++) { await Client.GetStringAsync("https://www.bing.com"); } } } |
The implemented client can reuse and take advantage of the property inside the parent class. This builds an ‘is a’ relationship to the parent class. Conceptually, one can say that a BingImplementedClient is a BaseClient because each subtype behaves like the parent class. This is classic inheritance which follows the SOLID Liskov substitution principle.
This tactic, however, is not without faults:
-
The static singleton is tight-coupled to the parent class
-
Clean up is a concern shared by the parent and all subtypes
-
All subtypes will need to be singletons because of class inheritance
Because of the ‘is a’ relationship between the parent and each subtype, the lifecycle of each client subtype will need to be a singleton. This is an unfortunate consequence of using inheritance to contain a static singleton that needs proper cleanup.
The HttpClient class instance gets glued to the parent class. As a rule, ‘new is glue’ and this is a design decision to tight-couple the class to its implementation details. This makes it hard to write any unit tests because it will make an actual network call with an open socket.
In this case, inheritance breaks encapsulation because each subtype needs to know intimate details about the parent class. When you instantiate a subtype, it will need to run the Dispose method inside the parent class.
To overcome these design deficiencies, a better approach is to delegate the primary concern. The one responsibility to care about is to make the HttpClient class a singleton at the app domain level.
This tactic calls for dependency injection with an IoC container.
Using Dependency Injection
This sample code will use .NET Core’s dependency injection IoC container to instantiate the singleton. This IoC container will manage the lifecycle of the singleton and do it in a manner that is thread-safe. This decision frees the programmer from having to solve this common problem.
The container class will use object composition and make the dependency instantiable through the constructor. This is a way to delegate concerns and stick to the single responsibility SOLID principle.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class BingClient { private readonly HttpClient _client; public BingClient(HttpClient client) { _client = client; } public async Task SendRequestToEndpoint() { for (var count = 0; count < 10; count++) { await _client.GetStringAsync("https://www.bing.com"); } } } |
Next, wire up the IoC container using Microsoft.Extensions.DependencyInjection which comes from a NuGet package. This package requires the following using statement:
1 |
using Microsoft.Extensions.DependencyInjection; |
To set up both the singleton and the container class, one can do it inside Program.cs or Startup.cs. The IoC set up can go wherever the initialization of the app goes, so for example:
1 2 3 4 5 6 |
var serviceProvider = new ServiceCollection() .AddSingleton<HttpClient>() .AddTransient<BingClient>() .BuildServiceProvider(); var bingClient = serviceProvider.GetService<BingClient>(); await bingClient.SendRequestToEndpoint(); |
Note, once the app domain is ready to end the running process, be sure to tell the IoC container to clean up:
1 |
serviceProvider.Dispose(); |
When you call the Dispose method on the service provider, the IoC container will dispose of any instance that implements IDisposable. This is a way to practice good hygiene right before the running process ends.
This form of delegation follows the dependency inversion SOLID principle. This principle says, “high-level modules shouldn’t depend on low-level modules but depend on abstractions.” The container class does not depend on a singleton instance but a class contract. This allows enough decoupling to depend on an abstraction without implementation details.
With the freedom to inject any HttpClient instance through the constructor, it is possible to mock the implementation details and write a unit test. This will ensure confidence that the code will work. Say you only care about the GetStringAsync method getting called exactly ten times.
The HttpClient class constructor can take in a delegate class called DelegatingHandler. This is an abstract class one can use to wire up a mock. The mock object can keep track of HTTP responses through an in-memory dictionary and increment a counter when it is called. The HttpClient class will delegate HTTP requests through the SendAsync method you can then override. This method has a CancellationToken parameter to halt the async operation, if necessary.
To implement the in-memory dictionary and return HTTP response codes, you will need the following using statements:
1 2 3 |
using System.Collections.Generic; using System.Net; using System.Threading; |
Now for the mock:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public class HttpMessageHandlerMock : DelegatingHandler { private readonly Dictionary<Uri, HttpResponseMessage> _responses; private int _callCount; public HttpMessageHandlerMock() { _responses = new Dictionary<Uri, HttpResponseMessage>(); _callCount = 0; } public void AddResponse(Uri uri, HttpResponseMessage responseMessage) { _responses.Add(uri, responseMessage); } public int CalledTimes { get { return _callCount; } } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return await Task.FromResult( new HttpResponseMessage(HttpStatusCode.InternalServerError)); } if (_responses.ContainsKey(request.RequestUri)) { _callCount++; return await Task.FromResult(_responses[request.RequestUri]); } return await Task.FromResult( new HttpResponseMessage(HttpStatusCode.NotFound)); } } |
This does everything the HttpClient class does minus sending an HTTP request. If the request matches one of the mock responses, then it returns the same request. This is useful for validating the request without opening any sockets during a unit test.
To fire up a unit test with this mock, you can dotnet new xunit in .NET Core. The dotnet new command sets up valid .NET Core projects. Be sure to add this using statement in addition to the using statements above:
1 |
using Xunit; |
Now for the unit test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class BingClientTests { private readonly HttpMessageHandlerMock _messageHandler; private readonly BingClient _client; public BingClientTests() { _messageHandler = new HttpMessageHandlerMock(); var httpClient = new HttpClient(_messageHandler); _client = new BingClient(httpClient); } [Fact] public async void CallsHttpClientTenTimes() { // arrange _messageHandler.AddResponse( new Uri("https://www.bing.com"), new HttpResponseMessage(HttpStatusCode.OK)); // act await _client.SendRequestToEndpoint(); // assert Assert.Equal(10, _messageHandler.CalledTimes); } } |
Using an IoC container to decouple a singleton instance from the HttpClient class is a good idea. This approach gives us both clean and testable code.
Conclusion
The HttpClient class can be one tricky beast to work with. I hope you see how dependency injection through an IoC container now saves you from pitfalls. In general, whatever path you chose should meet the following criteria:
-
The HttpClient should be a singleton instance throughout the lifecycle of the app domain
-
The singleton should be instantiated in a thread-safe way
-
Practice good hygiene and clean up the instance when done through the IDisposable interface
Keep in mind ‘new is glue’ and be aware of the design consequence when you instantiate the HttpClient class inside a container class. Or, use a proper IoC container to do the instantiation for you without reimagining the wheel.
Load comments