HttpClient - 这个实例已经启动了

29

我在我的API中使用HTTP客户端时遇到了以下异常:

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

而我将我的服务注入为

services.AddSingleton<HttpClient>()

我认为单例是我的最佳选择。我的问题可能是什么?
编辑:我的用法
class ApiClient
{
   private readonly HttpClient _client;
   public ApiClient(HttpClient client)
   {
      _client = client;
   }

   public async Task<HttpResponseMessage> GetAsync(string uri)
   {
     _client.BaseAddress = new Uri("http://localhost:5001/");
     _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json");
     var response = await _client.GetAsync(uri);

     return response;
   }
 }

2
也许可以发布整个类?目前我们不知道发生了什么。 - Luud van Keulen
使用AddScoped代替,以便获得一个新的请求实例。 - Kalten
1
信息很清楚,一旦您设置了BaseAddress等属性并发送请求,您就不能在之后更改这些属性。因此,单例模式是可以的,但前提是您也只能设置一次属性。 - Peter Bons
我将调用各种服务/端点,并需要相应地设置“BaseAddress”。这是否排除了“Singleton”? - rethabile
@Kalten 如果正在修改属性并且在同一范围内有多个请求,那么不建议使用AddScoped,因为会导致上述异常抛出。 - Nico
@no0b我建议使用“AddTransient”或创建自定义工厂(根据答案)。 - Nico
3个回答

59

这是HttpClient .Net Core源代码的类设计。

有趣的方法在于CheckDisposedOrStarted()

private void CheckDisposedOrStarted()
{
     CheckDisposed();
     if (_operationStarted)
     {
         throw new InvalidOperationException(SR.net_http_operation_started);
     }
}

现在当设置这些属性时会被调用:

  1. BaseAddress
  2. Timeout
  3. MaxResponseContentBufferSize

因此,如果您计划重复使用 HttpClient 实例,则应设置一个单独的实例,该实例预设这 3 个属性,并且所有使用必须不要修改这些属性。

或者您可以创建一个工厂或使用简单的AddTransient(...)。请注意,AddScoped 在这里不是最适合的,因为您将在每个请求作用域中收到相同的实例。

编辑基本工厂

现在,工厂只是负责为另一个服务提供实例的服务。以下是一个构建HttpClient的基本工厂,现在意识到这只是最基本的,您可以扩展此工厂以根据需要预设每个HttpClient的实例。

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

为什么我使用接口?这是因为使用依赖注入和控制反转,我们可以非常轻松地“交换”应用程序的部分。现在,我们访问 IHttpClientFactory 代替直接访问 HttpClientFactory

services.AddScoped<IHttpClientFactory, HttpClientFactory>();

现在在你的类、服务或控制器中,你需要请求工厂接口并生成一个实例。

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

readonly IHttpClientFactory _httpClientFactory;

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

这里的关键是。

  1. 工厂负责生成客户端实例并管理默认值。
  2. 我们请求接口而非实现,这有助于让组件保持独立以及实现更加模块化的设计。
  3. 该服务被注册为作用域实例。单例模式也有它们的用途,但在这种情况下,您更可能需要一个作用域实例。

作用域生命周期服务每个请求创建一次。


9
每个请求创建一个新的客户端将导致端口耗尽。 - Tratcher
@Tratcher 是的,它可以,但不是因为端口耗尽,而是因为插座耗尽。无论如何,这纯粹是为了举例。其他人应该阅读相关的问题和帖子,例如https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/。 - Nico
9
人们有模仿不良行为的习惯,请将其制止。 - Tratcher
1
从英语翻译为中文:
  1. aspnetmonsters.com文章标题:HttpClient使用错误的问题
  2. ankitvijay.net 文章标题:处理 HttpClient 实例:Dispose HttpClient 或拥有静态实例
- ablaze
1
这个回答出于好意,但可能不太适合:示例代码展示了如何不使用HttpClient。在这种特殊情况下,作用域必须是Singleton,否则,正如已经指出的那样,我们会遇到套接字耗尽的问题。 - Mir

15

单例模式是正确的方法。使用作用域或瞬态会阻止连接池并导致性能下降和端口耗尽。

如果您有一致的默认值,则可以在注册服务时初始化它们:

        var client = new HttpClient();
        client.BaseAddress = new Uri("http://example.com/");
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        services.AddSingleton<HttpClient>(client);

...

        var incoming = new Uri(uri, UriKind.Relative); // Don't let the user specify absolute.
        var response = await _client.GetAsync(incoming);

如果您没有一致的默认值,则不应使用BaseAddress和DefaultRequestHeaders。相反,应该创建一个新的HttpRequestMessage:

        var incoming = new Uri(uri, UriKind.Relative); // Don't let the user specify absolute urls.
        var outgoing = new Uri(new Uri("http://example.com/"), incoming);
        var request = new HttpRequestMessage(HttpMethod.Get, outgoing);
        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        var response = await _client.SendAsync(request);

我的应用程序与几个(假设有十二个)不同域上的几个(假设有五个)API进行交互。在我的DI容器中为每个API旋转一个单独的单例HttpClient对象是否合适?对于每个域呢?还是应该为每个API创建一个单独的HttpRequestMessage? - Dan Narsavage
1
无论哪种方式最方便,都不会影响行为。HttpClient在内部通过域名分组连接。只要您为特定域重复使用同一客户端,那么无论您是否将一个客户端用于多个域,都没有关系(直到您需要不同的设置)。 - Tratcher
我只是留下这个链接到 .Net Core v3.1 文档,供参考。https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=netcore-3.1#examples 示例代码的第一行声明; // HttpClient 旨在每个应用程序实例化一次,而不是每次使用。请参阅备注。(\r\n) static readonly HttpClient client = new HttpClient(); ... - Scott Fraley

0

最好将请求的URL和头信息添加到消息中,而不是在客户端上添加。除非必须使用BaseAddressDefaultRequestHeaders,否则最好不要使用它们。

HttpRequestMessage yourmsg = new HttpRequestMessage {
    Method = HttpMethod.Put,
    RequestUri = new Uri(url),
    Headers = httpRequestHeaders;
};


httpClient.SendAsync(yourmsg);

对于重复使用单个HttpClient进行多个请求,它的效果非常好。


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