HttpClient: 此实例已经启动了一个或多个请求。属性只能在发送第一个请求之前修改。

4
我有一个ASP.NET MVC应用程序,每当UI中的一个按钮被按下时,就会调用一个ASP.NET Web API REST服务。
每次这个按钮被按下时,下面的方法就会被执行。
public class MyClass
{
    private static HttpClient client = new HttpClient();

    public async Task DumpWarehouseDataIntoFile(Warehouse myData, string path, string filename)
    { 
        try
        {
           //Hosted web API REST Service base url  
           string Baseurl = "http://XXX.XXX.XX.X:YYYY/";  

           //using (var client = new HttpClient())  --> I have declared client as an static variable
           //{  
            //Passing service base url  
            client.BaseAddress = new Uri(Baseurl);  

            client.DefaultRequestHeaders.Clear();  

            //Define request data format  
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));  
              
            // Serialize parameter to pass to the asp web api rest service
            string jsonParam = Newtonsoft.JsonConvert.SerializeObject(myData);

            //Sending request to find web api REST service resource  using HttpClient
            var httpContent = new StringContent(jsonParam, Encoding.UTF8, "application/json");

            HttpResponseMessage Res = await client.PostAsync("api/Warehouse/DumpIntoFile", httpContent);  

            //Checking the response is successful or not which is sent using HttpClient  
            if (Res.IsSuccessStatusCode)  
            {  
               // Some other sftuff here
            }
           //}
         }
         catch (Exception ex)
         {
              // Do some stuff here
         } // End Try

    } // End DumpWarehouseDataIntoFile method
} // End class

Warehouse 类对象:

public class Warehouse
{
    public DataTable dt { get; set; }
    public string Filepath { get; set; }
}

我在这篇文章中发现了该模式:
using (var myClient = new HttpClient())
{
}

不建议使用System.Net.Sockets.SocketException,因为它会导致套接字耗尽。建议使用HttpClient作为静态变量并重用它,以减少套接字浪费。因此,我使用了一个静态变量。

这种方法的问题(在我的情况下)是它只能在第一次按下按钮时起作用,下一次按下按钮并执行DumpWarehouseDataIntoFile方法时,会抛出以下异常:

执行请求时发生未处理的异常。 System.InvalidOperationException:此实例已经启动了 一个或多个请求。仅在发送第一个请求之前才能修改属性。

正如错误所说,像基本地址等属性只能在发送第一个请求之前修改一次。

我进行了谷歌搜索并找到了一些解决方案:

第一个解决方案

因此,单例模式似乎是一个不错的选择,就像Alper提出的这里提出的那样。以下是Alper提出的单例:

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 + "/";
    }
}

使用方法

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

我在这里看到的问题是,如果您多次调用上面的代码(在我的情况下,每次按下UI上的按钮并调用DumpWarehouseDataIntoFile方法),则会创建MyApiClient的实例,并因此创建了一个新的HttpClient实例,我想重用HttpClient,而不是创建许多实例。

第二种解决方案

创建一种工厂,如Nico在这里所提出的。以下是他提出的代码:

public interface IHttpClientFactory
{
    HttpClient CreateClient();
}

public class HttpClientFactory : IHttpClientFactory
{
    static string baseAddress = "http://example.com";

    public HttpClient CreateClient()
    {
        var client = new HttpClient();
        SetupClientDefaults(client);
        return client;
    }

    protected virtual void SetupClientDefaults(HttpClient client)
    {
        client.Timeout = TimeSpan.FromSeconds(30); //set your own timeout.
        client.BaseAddress = new Uri(baseAddress);
    }
}

使用方法

public HomeController(IHttpClientFactory httpClientFactory)
{
    _httpClientFactory = httpClientFactory;
}

readonly IHttpClientFactory _httpClientFactory;

public IActionResult Index()
{
    var client = _httpClientFactory.CreateClient();
    //....do your code
    return View();
}

每次调用CreateClient时,都会创建一个新的HttpClient实例。您不会重复使用HttpClient对象。

第三种解决方案

使用IHttpClientFactory进行HTTP请求,如此处所述。 问题在于它仅适用于.NET Core,而非标准ASP.NET Framework,尽管似乎可以通过安装此NuGet包来获取该功能。它似乎可以自动高效地管理 HttpClient实例,并且我想将其应用于我的场景中。我希望避免重复发明轮子。

我从未使用过IHttpClientFactory,也不知道如何使用它:配置一些特性,比如基地址、设置请求头,创建HttpClient实例,然后调用PostAsync方法,将HttpContent作为参数传递。

我认为这是最好的方法,所以能否告诉我我需要做哪些必要的步骤,以便像在DumpWarehouseDataIntoFile方法中一样使用IHttpClientFactory进行相同的操作?我有点迷失,不知道如何将IHttpClientFactory应用于DumpWarehouseDataIntoFile方法。

任何未在此处提出的其他解决方案以及一些代码片段都将受到高度赞赏。

3个回答

4

HttpClient

HttpClient 可能会在以下情况下抛出 InvalidOperationException 异常:

  • 当请求已经发送后调用 BaseAddress 设置器
  • 当请求已经发送后调用 Timeout 设置器
  • 当请求已经发送后调用 MaxResponseContentBufferSize 设置器
  • 当操作已经开始并且重新发送被请求时

为了避免这些问题,您可以在每个请求级别上设置前两个,例如:

CancellationTokenSource timeoutSource = new CancellationTokenSource(2000);
await httpClient.GetAsync("http://www.foo.bar", timeoutSource.Token);

HttpClientFactory

您可以使用以下技巧在.NET Framework中使用IHttpClientFactory:

  • AddHttpClientDefaultHttpClientFactory注册为IHttpClientFactory
  • 然后您可以从DI容器中检索它
var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider();
container.RegisterInstance(serviceProvider.GetService<IHttpClientFactory>());
container.ContainerScope.RegisterForDisposal(serviceProvider);

这个示例使用了 SimpleInjector,但是同样的概念也可以应用于任何其他依赖注入框架。

1
最好在消息中添加请求URL和标头。除非您有默认要求,否则不要使用httpClient.BaseAddresshttpClient.DefaultRequestHeaders
HttpRequestMessage msg = new HttpRequestMessage {
    Method = HttpMethod.Put,
    RequestUri = new Uri(url),
    Headers = httpRequestHeaders;
};

httpClient.SendAsync(msg);

它很适合在多个请求中重复使用HttpClient


0

我不确定,但如果您将这些行移动到构造函数中会发生什么:

        //Passing service base url  
        client.BaseAddress = new Uri(Baseurl);  

        client.DefaultRequestHeaders.Clear();  

        //Define request data format  
        client.DefaultRequestHeaders.Accept
       .Add(new MediaTypeWithQualityHeaderValue("application/json"));  

我认为重新初始化是个问题。


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