46

I discovered that a single HttpClient could be shared by multiple requests. If shared, and the requests are to the same destination, multiple requests could reuse the connections. WebRequest needs to recreate the connection for each request.

I also looked up some documentation on other ways to use HttpClient in examples.

The following article summarizes the high-speed NTLM-authenticated connection sharing: HttpWebRequest.UnsafeAuthenticatedConnectionSharing  

Possible implementations that I tried out are shown below

A)

private WebRequestHandler GetWebRequestHandler()
{
    CredentialCache credentialCache = new CredentialCache();
    credentialCache.Add(ResourceUriCanBeAnyUri, "NTLM", CredentialCache.DefaultNetworkCredentials);
    WebRequestHandler handler = new WebRequestHandler
    {
        UnsafeAuthenticatedConnectionSharing = true,
        Credentials = credentialCache
    };

    return handler;
}

using (HttpClient client = new HttpClient(GetWebRequestHandler(), false))
{
}

B)

using (HttpClient client = new HttpClient)
{
}

C)

HttpWebRequest req = (HttpWebRequest)WebRequest.Create("some uri string")

I would appreciate any help in making me understand which approach I should take so as to achieve max performance, minimizing connections and making sure security is not impacted.

Wyck
  • 10,311
  • 6
  • 39
  • 60
StackOverflowVeryHelpful
  • 2,347
  • 8
  • 34
  • 46
  • HttpClient is the new cool kid in town, and it's supposedly the best of all, supports async/tasks, and is much more portable than others (there is also WebClient). However it requires .NET 4.5+. That being said, I don't think you should see much differences between them in terms of raw performance when used properly. – Simon Mourier Aug 31 '16 at 06:56
  • 7
    I you use HttpClient have a look at this post [YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE](http://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/) – George Vovos Aug 31 '16 at 16:52
  • definitely go with HttpClient, apart from the fact that it handles connection pooling, async/await out of the box, it offers more flexibility via them Handlers and is also easier to write your unit tests with HttpClient. – Duy Sep 05 '16 at 01:19
  • 3
    HttpClient is intended to be instantiated once and re-used throughout the life of an application. Especially in server applications, creating a new HttpClient instance for every request will exhaust the number of sockets available under heavy loads. This will result in SocketException errors. Example: https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client – SergeyT Jan 04 '17 at 00:16
  • Have a look at this first : https://learn.microsoft.com/en-us/archive/blogs/timomta/controlling-the-number-of-outgoing-connections-from-httpclient-net-core-or-full-framework the DefaultConnectionLimit parameter could leads to a client side bottleneck – Larry Jan 31 '20 at 16:13

3 Answers3

42

If you use either of them with async it should be good for the performance point of view as it will not block the resources waiting for the response and you will get good throughput.

HttpClient is preferred over HttpWebRequest due to async methods available out of the box and you would not have to worry about writing begin/end methods.

Basically when you use async call (using either of the class), it will not block the resources waiting for the response and any other request would utilise the resources to make further calls.

Another thing to keep in mind that you should not be using HttpClient in the 'using' block to allow reuse of same resources again and again for other web requests.

See following thread for more information

Do HttpClient and HttpClientHandler have to be disposed?

Community
  • 1
  • 1
Sujit Singh
  • 799
  • 6
  • 11
8

This is my ApiClient which creates the HttpClient for only once. Register this object as singleton to your dependency injection library. It's safe to reuse because it's stateless. Do NOT recreate HTTPClient for each request. Reuse Httpclient as much as possible

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);   
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}

The usage;

using ( var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}
Alper Ebicoglu
  • 8,884
  • 1
  • 49
  • 55
0
  1. There is a problem in your implementation 'A'. The lifetime of the instance returned from GetWebRequestHandler() is shortlived (maybe just for the sake of the example?). If this was done on purpose, it negates the passing of false for the 2nd param of HttpClient constructor. The value of false tells the HttpClient to not dispose the underlying HttpMessageHandler (which helps with scaling since it does not close the port for the request). This is, of course, assuming that the lifetime of the HttpMessageHandler is long enough for you to take advantage of the benefit of not opening/closing ports (which is a big impact on the scalability of your server). Thus, I have a recommendation below of option 'D'.

  2. There is also an option 'D' that you do not list above - to make the Httpclient instance static and re-used across all api calls. This is much more efficient from a memory allocation and GC perspective - as well as the opening of ports on the client. You don't have the overhead of allocating memory for and creating HttpClient instances (and all of its underlying objects) and thus avoid the cleanup via GC for them too.

Please refer to my answer provided about a similar question - What is the overhead of creating a new HttpClient per call in a WebAPI client?

Dave Black
  • 7,305
  • 2
  • 52
  • 41