Saturday, November 29, 2014

.NET 4.5 HttpClient is Thread Safe

Good news everyone, the .NET 4.5 HttpClient is thread safe!

This means that you can share instances of your HttpClients across your entire application. This is useful in that it allows you to reuse persisted connections. One of the best ways to do this is to create a class that can manage the object lifetime of those clients for you.

Below is a simple HttpClientManager that will create one HttpClient per authority. Why per authority? Because you might have to have different settings or credentials for different websites.

Sample Unit Tests

public class HttpClientManagerTests
{
    [Fact]
    public void GetForAuthority()
    {
        using (var manager = new HttpClientManager())
        {
            var client1 = manager.GetForAuthority("http://tomdupont.net/");
            var client2 = manager.GetForAuthority("https://tomdupont.net/");
            Assert.Same(client1, client2);
 
            var client3 = manager.GetForAuthority("http://google.com/");
            Assert.NotSame(client1, client3);
        }
    }
 
    [Fact]
    public void TryRemoveForAuthority()
    {
        const string uri = "http://tomdupont.net/";
 
        using (var manager = new HttpClientManager())
        {
            Assert.False(manager.TryRemoveForAuthority(uri));
 
            manager.GetForAuthority(uri);
 
            Assert.True(manager.TryRemoveForAuthority(uri));
 
        }
    }
}

HttpClientManager

public interface IHttpClientManager : IDisposable
{
    HttpClient GetForAuthority(Uri uri);
 
    bool TryRemoveForAuthority(Uri uri);
}
 
public class HttpClientManager : IHttpClientManager
{
    private readonly ConcurrentDictionary<string, HttpClient> _clientMap
        = new ConcurrentDictionary<string, HttpClient>();
 
    private readonly Func<Uri, HttpClient> _clientFactory;
 
    private bool _isDisposed;
 
    public HttpClientManager(Func<Uri, HttpClient> clientFactory = null)
    {
        _clientFactory = clientFactory ?? (uri => new HttpClient());
    }
 
    public HttpClient GetForAuthority(Uri uri)
    {
        return _clientMap.GetOrAdd(uri.Authority, k => _clientFactory(uri));
    }
 
    public bool TryRemoveForAuthority(Uri uri)
    {
        HttpClient client;
        var result = _clientMap.TryRemove(uri.Authority, out client);
 
        if (result)
            DisposeClient(client);
 
        return result;
    }
 
    public void Dispose()
    {
        if (_isDisposed)
            return;
 
        foreach (var client in _clientMap.Values)
            DisposeClient(client);
 
        _clientMap.Clear();
 
        _isDisposed = true;
    }
 
    private static void DisposeClient(HttpClient client)
    {
        client.CancelPendingRequests();
        client.Dispose();
    }
}
 
public static class HttpClientManagerExtensions
{
    public static HttpClient GetForAuthority(
        this IHttpClientManager manager, 
        string uriString)
    {
        var uri = new Uri(uriString);
        return manager.GetForAuthority(uri);
    }
 
    public static bool TryRemoveForAuthority(
        this IHttpClientManager manager,
        string uriString)
    {
        var uri = new Uri(uriString);
        return manager.TryRemoveForAuthority(uri);
    }
}

Enjoy,
Tom

No comments:

Post a Comment

Real Time Web Analytics