当每个请求需要新的授权头时,在Web应用程序中重用HttpClient

3
我已经阅读了很多关于HttpClient实例应该尽可能地重用,甚至在整个应用程序生命周期中都重用的文章。为了完整起见,以下是我基于以下几个资源发表我的观点:

我有几个问题:

  1. 如何在ASP.NET MVC中创建一个应用程序级别的HttpClient实例,以在所有请求之间共享?假设没有IoC容器,因此我不能只将其绑定到Singleton作用域并结束。我该如何手动完成?
  2. 此外,我正在与之交互的Web服务需要在每个请求上提供一个新的授权令牌,所以即使我想出了上述#1的方法,我如何在每个请求上提供新的授权标头,以便它不会与潜在的多个并发请求(来自不同的用户等)发生冲突?我了解到HttpClient本身在GetAsync和其他方法方面相当线程安全,但是设置DefaultAuthorizationHeaders对我来说似乎不是线程安全的,是吗?
  3. 如何保持它可单元测试?

这是我的正在进行中的代码(为了简洁起见,在此处略有简化):

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();
    }
}

顺便提一下:我在这里使用了依赖注入,但不一定是一个IoC容器(由于与本讨论无关的原因)。

2个回答

0

在进行了一些额外的在线阅读后,我得出了这个结论:

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;
    }

}

唯一的问题是,它不能进行单元测试。我无法用一个虚拟的http客户端替换它。我可以将_httpClient封装在一个属性中,并使其非只读。这样,通过属性设置器,httpclient就可以从单元测试中被覆盖。但我不确定我是否喜欢这个解决方案。
另一个想法是通过属性对静态的_httpClient进行延迟初始化,但我不确定这是否更好。
对于这两个想法有什么想法吗?还有其他想法吗?

1
假设您的应用程序是异步的,如果多个线程进入GetDataByQuery并且它们都更改DefaultRequestHeaders.Authorization会发生什么? 我认为您可以创建一个HttpRequestMessage对象,在其中设置授权标头,最后调用_httpClient.SendAsync(requestMessage)。 - Jón Trausti Arason
是的,说得好。我实际上曾经意识到同样的事情,并开始按照你描述的方式去做。 - Jiveman

0

我决定以稍微不同的方式来做这件事,以便我可以进行单元测试。我在这里使用属性注入来允许在单元测试中覆盖 Http 客户端。但在生产代码中,它将在第一次访问 Client 属性时自动初始化(惰性)。

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;
    }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接