3

I've read a good deal about how HttpClient instances should be reused as much as possible, perhaps even throughout application lifecycle. For completeness sake, here are a couple of the resources that I'm basing my statements on:

I have a couple of questions regarding this:

  1. How do I create an application-scoped instance of HttpClient in ASP.NET MVC to be shared among all requests? Let's assume there is no IoC container in picture, so I can't just bind it in Singleton scope with container-name-here and call it a day. How would I do it "manually?"
  2. Also, the web service I'm interacting with requires a new authorization token on each request, so even if come up with a way to do #1 above, how do I supply a new authorization header on every request, so that it doesn't collide with potential multiple concurrent requests (coming from different users and whatnot)? My understanding is that HttpClient is fairly thread-safe itself, when it comes to GetAsync and other methods, but setting DefaultAuthorizationHeaders doesn't seem thread-safe to me, is it?
  3. How do I keep it unit-testable?

This is how my in-progress code looks like so far (in a somewhat simplified form for brevity here):

public class MyHttpClientWrapper : IDisposable
{
    private readonly HttpClient _httpClient;
    private readonly TokenManager _tokenManager;

    public HttpServiceClient(HttpClient httpClient, TokenManager tokenManager)
    {
        _httpClient = httpClient;
        _tokenManager = tokenManager;

        _httpClient.BaseAddress = new Uri("https://someapp/api/");
        _httpClient.DefaultRequestHeaders.Accept.Add(new 
            MediaTypeWithQualityHeaderValue("application/json"));
    }

    public string GetDataByQuery(string query)
    {
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "amx", _tokenManager.GetNewAuthorizationCode());

        var response = _httpClient.GetAsync(query).Result;
        return response.Content.ReadAsStringAsync().Result;
    }

    public void Dispose()
    {
        HttpClient?.Dispose();
    }
}

Side note: I am using dependency injection here, but not necessarily an IoC container (for the reasons irrelevant to this discussion).

Community
  • 1
  • 1
Jiveman
  • 1,022
  • 1
  • 13
  • 30

2 Answers2

0

After some additional reading online, I came up with this:

public class MyHttpClientWrapper
{
    private static readonly HttpClient _httpClient;
    private readonly TokenManager _tokenManager;

    static MyHttpClientWrapper()
    {
        // Initialize the static http client:
        _httpClient = new HttpClient();
        _httpClient.BaseAddress = new Uri("https://someapp/api/");
        _httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    }

    public HttpServiceClient(TokenManager tokenManager)
    {
        _tokenManager = tokenManager;        
    }

    public string GetDataByQuery(string query)
    {
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "amx", _tokenManager.GetNewAuthorizationCode());

        var response = _httpClient.GetAsync(query).Result;
        return response.Content.ReadAsStringAsync().Result;
    }

}

The only problem is, it's not unit-testable. I have no way of replacing the http client with a fake one. I could encapsulate _httpClient in a property and make it non-readonly. That way the httpclient could be overwritten from a unit test via the property setter. I'm not sure I'm in love with that solution.

Another idea is to do lazy initialization of the static _httpClient via a property, but I'm not sure if that is better.

Any thoughts about either of those ideas? Any other thoughts?

Jiveman
  • 1,022
  • 1
  • 13
  • 30
  • 1
    Assuming your app is asynchronous, what would happen if multiple threads enter GetDataByQuery and they all change the DefaultRequestHeaders.Authorization? I believe you can create a HttpRequestMessage object, set the authorization header on it, and finally call _httpClient.SendAsync(requestMessage) – Jón Trausti Arason Nov 19 '16 at 18:19
  • Yes, good point. I had actually realized the same thing at one point, and started doing exactly what you described there. – Jiveman Nov 21 '16 at 19:11
0

I decided to do this slightly differently, so that I can allow unit-testing. I'm using property injection here to allow for overriding the Http Client in unit tests. But in production code, it would simply get self-initialized (lazy) on first access of the Client property.

public class MyHttpClientWrapper
{
    private static readonly object ThreadLock = new object();
    private static HttpClient _httpClient;
    private readonly TokenManager _tokenManager;

    public Client
    {
        get
        {
            if (_httpClient != null) return _httpClient;

            // Initialize http client for the first time, and lock for thread-safety
            lock (ThreadLock)
            {
                // Double check
                if (_httpClient != null) return _httpClient;

                _httpClient = new HttpClient();
                InitClient(_httpClient);
                return _httpClient;
            }

        }
        set
        {
            // primarily used for unit-testing
            _httpClient = value;
            InitClient(_httpClient);
        }
    }

    private void InitClient(HttpClient httpClient)
    {
        httpClient.BaseAddress = new Uri("https://someapp/api/");
        httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    }


    public HttpServiceClient(TokenManager tokenManager)
    {
        _tokenManager = tokenManager;        
    }

    public string GetDataByQuery(string query)
    {
        Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "amx", _tokenManager.GetNewAuthorizationCode());

        var response = _httpClient.GetAsync(query).Result;
        return response.Content.ReadAsStringAsync().Result;
    }

}
Jiveman
  • 1,022
  • 1
  • 13
  • 30