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

时间:2016-09-02 17:08:52

标签: c# asp.net-mvc http-headers dotnet-httpclient

我已经阅读了很多关于如何尽可能多地重用HttpClient实例的内容,甚至可能在整个应用程序生命周期中。为了完整起见,这里有几个我基于我的陈述的资源:

我对此有几个问题:

  1. 如何在ASP.NET MVC中创建一个应用程序范围的HttpClient实例,以便在所有请求之间共享?让我们假设图片中没有IoC容器,所以我不能只用 container-name-here 将它绑定在Singleton范围内并将其称为一天。我该怎么做“手动?”
  2. 此外,我正在与之交互的网络服务需要每个请求上的新授权令牌,所以即使想出一个方法来执行上面的#1,如何在每个请求上提供新的授权标头,以便它不会与潜在的多个并发请求(来自不同的用户和诸如此类)发生冲突?我的理解是,当涉及到GetAsync和其他方法时,HttpClient本身就是相当线程安全的,但设置DefaultAuthorizationHeaders对我来说似乎不是线程安全的,是吗?
  3. 如何保持单元可测试?
  4. 到目前为止,这是我正在进行的代码的样子(为了简洁起见,在某种简化的形式中):

    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 :(得分: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封装在一个属性中,并使其成为非readonly。这样,httpclient可以通过属性设置器从单元测试中覆盖。我不确定我是否喜欢这个解决方案。

另一个想法是通过属性对静态_httpClient进行延迟初始化,但我不确定这是否更好。

对这些想法中的任何一个有何看法?还有其他想法吗?

答案 1 :(得分: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;
    }

}