【问题标题】:HttpClient - This instance has already startedHttpClient - 此实例已启动
【发布时间】:2017-07-03 07:19:21
【问题描述】:

我在我的 api 中使用 http 客户端遇到了这个异常。

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

我将我的服务注入为

services.AddSingleton<HttpClient>()

我认为单身是我最好的bet。我的问题可能是什么?

编辑:我的用法

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

【问题讨论】:

  • 也许发布整个课程?我们现在不知道发生了什么。
  • 使用AddScoped 来按请求获取不同的实例。
  • 消息很清楚,一旦您设置了 BaseAddress 之类的属性并发出请求,之后您将无法更改这些属性。所以单例是可以的,但前提是你也设置了一次属性。
  • 我将调用各种服务/端点,并且需要相应地设置BaseAddress。这是否排除了Singleton
  • @Kalten AddScoped 如果属性正在被修改并且同一范围内的多个请求会导致上面抛出的异常,则不建议使用。

标签: c# asp.net-core


【解决方案1】:

这是HttpClient .Net Core Source类的设计。

这里有趣的方法是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);
    }
}

现在我为什么要使用和接口?这是通过使用依赖注入和 IoC 来完成的,我们可以很容易地“交换”应用程序的某些部分。现在我们不再尝试访问HttpClientFactory,而是访问IHttpClientFactory

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. 服务已注册为 Scoped 实例。单例有它们的用途,但在这种情况下,您更有可能需要一个作用域实例。

范围内的生命周期服务为每个请求创建一次。

【讨论】:

【解决方案2】:

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

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

        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 吗?
  • 哪个最方便,不会影响行为。内部 HttpClient 按域对连接进行分组。只要您为给定域重复使用相同的客户端,是否将一个客户端用于多个域都没有关系(除非您需要不同的设置)。
  • 我只是将此链接留给 .Net Core v3.1 文档供参考。 https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=netcore-3.1#examples 示例代码第一行说明; // HttpClient is intended to be instantiated once per application, rather than per-use. See Remarks. (\r\n) static readonly HttpClient client = new HttpClient(); ...
【解决方案3】:

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

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


httpClient.SendAsync(yourmsg);

它适用于为多个请求重用单个 HttpClient

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-04-26
    • 1970-01-01
    • 2018-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多