Async programming has become ubiquitous and the standard tool for making async HTTP requests with C# is HttpClient
from the System.Net.Http
namespace. Examples are aplenty, but good examples are few and far between. Because HttpClient
implements IDisposable
, we are conditioned to new it up in a using
statement, make the call and get out as fast as possible.
This is WRONG.
This blog post will pull together advice from the few good resources online that touch on various aspects of why you are using it wrong, and how to use it correctly.
HttpClient
is designed for re-use. You should use a single instance of it throughout the lifetime of your application, or at least as few as possible when request signatures will vary (explained later). The main reason for this is that each instance of HttpClient
will open a new socket connection and on high traffic sites, you can exhaust the available pool and receive a System.Net.Sockets.SocketException
.
Additionally, by re-using instances, you can avoid the not-insignificant overhead of establishing new TCP connections because HttpClient
can re-use its existing connections, and do so in a thread safe manner.
So, if : using(var client = new HttpClient())
is wrong, what do we do?
You create a single static
instance of HttpClient
. In console or desktop applications, you can expose this instance just about anywhere. In an ASP.NET Web API application, you can create it in the Global.asax.cs file.
Recipe: Your Web API always uses the same header values and the same base URL
Global.asax.cs:
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Web.Http;
namespace HttpClientGuidance
{
public class WebApiApplication : System.Web.HttpApplication
{
internal static HttpClient httpClientInstance;
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
Uri baseUri = new Uri("https://someresource.com");
httpClientInstance = new HttpClient();
httpClientInstance.BaseAddress = baseUri;
httpClientInstance.DefaultRequestHeaders.Clear();
httpClientInstance.DefaultRequestHeaders.ConnectionClose = false;
httpClientInstance.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
ServicePointManager.FindServicePoint(baseUri).ConnectionLeaseTimeout = 60 * 1000;
}
}
}
Here we have set up the client one time. All uses of it will:
- Use the same base address. You can tack on varying routes (e.g. /api/somecontroller, /api/someothercontroller) without issue.
- Set the request
content-type
to application/json - Will attempt to keep the connection open (
.ConnectionClose = false
) which makes more efficient use of the client.
Since keeping connections open can prevent load balancing, we compensate by setting a connection lease timeout on the base URL through the ServicePointManager
. This ensures connections are used efficiently but not indefinitely.
The ConnectionLeaseTimeout
has an additional bonus. Since HttpClient
will cache DNS data, you could run into a situation when a DNS change breaks your code because an instance of HttpClient
is still using DNS from before the change. This can happen particularly when using a Blue/Green deployment strategy where you deploy an update of your app to production but don’t make it live until flipping DNS – and your base URI depends on those DNS settings. Setting the timeout will minimize downtime.
Basic Usage Example:
using HttpClientGuidance.Models;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
namespace HttpClientGuidance.Controllers
{
public class BasicClientUsageController : ApiController
{
public async Task<HttpResponseMessage> Post()
{
Fruit grape = new Fruit
{ FruitName = "Grape", FruitColor = "Purple" };
return await WebApiApplication.httpClientInstance
.PostAsJsonAsync("/api/somecontroller", grape);
}
}
}
RECIPE: YOUR WEB API uses a few different configurations
While HttpClient
is thread safe, not all of its properties are. You can cause some very difficult to identify bugs by re-using the same instance but changing the URL and/or headers before each call. If this configuration will vary, then you cannot use the first recipe.
If you have a manageable number of configurations, create an HttpClient
instance for each one. You will be slightly less efficient than an single instance, but significantly more efficient than creating and disposing each time.
internal static HttpClient httpClientInstance;
internal static HttpClient twitterClient;
internal static HttpClient bitcoinExchngClient;
In this way, you can re-use the client instances as appropriate. Just set all the headers, base URLs, and ServicePointManager
connection timeouts individually.
RECIPE: You have many different APIs to call and maybe you even often add new ones with each software release (or you just like this method best).
In this case, it can become unmanageable to create separate client instances for all of the HTTP calls you make. You can still gain all the advantages of a single client instance by varying your HttpRequestMessage
instead of your HttpClient
.
Observe our new Global.asax.cs file:
using System.Net;
using System.Net.Http;
using System.Web.Http;
namespace HttpClientGuidance
{
public class WebApiApplication : System.Web.HttpApplication
{
internal static HttpClient httpClientInstance;
protected void Application_Start()
{
GlobalConfiguration.Configure(WebApiConfig.Register);
httpClientInstance = new HttpClient();
httpClientInstance.DefaultRequestHeaders.ConnectionClose = false;
ServicePointManager.FindServicePoint
("some uri").ConnectionLeaseTimeout = 60 * 1000;
ServicePointManager.FindServicePoint
("some other uri").ConnectionLeaseTimeout = 60 * 1000;
ServicePointManager.FindServicePoint
("some other other uri").ConnectionLeaseTimeout = 60 * 1000;
}
}
}
You can see that we have eliminated the content type and the base URI because that is going to change often in our use of the client.
Also, we have configured the ConnectionLeaseTimeout
for all the URI’s that we know we will be using. If you don’t know all the URIs at design time, then it is okay to do this at runtime – at least it is better to do it at runtime than not at all.
To make use of this single client instance, you will create an HttpRequestMessage
with all the configuration that you would have set directly on the HttpClient if it were always going to be the same. Then you make the request through the client’s SendAsync
method.
Since it is so common in ASP.NET Web API to send object instances as request content, I am demonstrating the use of ObjectContent but any of the derivatives of HttpContent can be used, StringContent also being very common.
using HttpClientGuidance.Models;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web.Http;
namespace HttpClientGuidance.Controllers
{
public class BasicClientUsageController : ApiController
{
public async Task<HttpResponseMessage> Post()
{
Fruit grape = new Fruit { FruitName = "Grape", FruitColor = "Purple" };
var msg = new HttpRequestMessage(HttpMethod.Post, "http://someurl");
msg.Content = new ObjectContent<Fruit>
(grape, new JsonMediaTypeFormatter(), (MediaTypeHeaderValue)null);
msg.Headers.Authorization = new AuthenticationHeaderValue
("whatever scheme", "whatever value");
msg.Headers.Add("some custom hdr", "some value");
return await WebApiApplication.httpClientInstance.SendAsync(msg);
}
}
}
While this clearly does not demonstrate all of the options on HttpRequestMessage
, the point is that all of the configuration can be done on the message without affecting the efficiency of using a single instance of the client. So you can feel free to create as many varying request messages as you need.
Summing Up
I hope this sheds some light on a topic that is under-documented and yet highly-relevant to .NET developers that are using APIs (is anyone not?).
Despite all you have seen and been taught, do not dispose your HttpClient
instance(s). Okay, if your app is some utility app that would never come close to exhausting connections, then it doesn’t really matter as long as you know how to do it right when the time comes. But generally, re-use it, don’t abuse it.
Special thanks to the following resources that I used to pull this together: