在 HttpClient 上使用 DelegatingHandler 和自定义数据

Wil*_*ker 3 c# dotnet-httpclient .net-core

考虑到使用 HttpClient 的众所周知的困境和问题 - 即套接字耗尽和不尊重 DNS 更新,最好的做法是使用 IHttpClientFactory 并让容器决定何时以及如何利用 http 池连接效率。这一切都很好,但现在我无法在每个请求上使用自定义数据实例化自定义 DelegatingHandler。

下面的示例说明了我在使用工厂方法之前是如何做到的:

public class HttpClientInterceptor : DelegatingHandler
{
    private readonly int _id;
    public HttpClientInterceptor(int id)
    {
        _id = id;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // associate the id with this request
        Database.InsertEnquiry(_id, request);
        return await base.SendAsync(request, cancellationToken);
    }
}
Run Code Online (Sandbox Code Playgroud)

每次实例化 HttpClient 时,都可以传递一个 Id:

public void DoEnquiry()
{
    // Insert a new enquiry hypothetically 
    int id = Database.InsertNewEnquiry();
    using (var http = new HttpClient(new HttpClientInterceptor(id)))
    {
        // and do some operations on the http client
        // which will be logged in the database associated with id
        http.GetStringAsync("http://url.com");
    }
}
Run Code Online (Sandbox Code Playgroud)

但现在我无法实例化 HttpClient 或处理程序。

public void DoEnquiry(IHttpClientFactory factory)
{
    int id = Database.InsertNewEnquiry();
    var http = factory.CreateClient();
    // and now??

    http.GetStringAsync("http://url.com");
}
Run Code Online (Sandbox Code Playgroud)

我如何才能使用工厂实现类似的目标?

Ste*_*ary 5

我无法在每个请求上使用自定义数据实例化自定义 DelegatingHandler。

这是对的。但是您可以使用可重用(且无状态)的自定义DelegatingHandler,并将数据作为请求的一部分传递。这就是HttpRequestMessage.Properties目的。在执行这些类型的“自定义上下文”操作时,我更喜欢将上下文“属性”定义为扩展方法:

public static class HttpRequestExtensions
{
  public static HttpRequestMessage WithId(this HttpRequestMessage request, int id)
  {
    request.Properties[IdKey] = id;
    return request;
  }

  public static int? TryGetId(this HttpRequestMessage request)
  {
    if (request.Properties.TryGetValue(IdKey, out var value))
      return value as int?;
    return null;
  }

  private static readonly string IdKey = Guid.NewGuid().ToString("N");
}
Run Code Online (Sandbox Code Playgroud)

然后你可以在(可重用)中使用它DelegatingHandler

public class HttpClientInterceptor : DelegatingHandler
{
  protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  {
    var id = request.TryGetId();
    if (id == null)
      throw new InvalidOperationException("This request must have an id set.");

    // associate the id with this request
    Database.InsertEnquiry(id.Value, request);
    return await base.SendAsync(request, cancellationToken);
  }
}
Run Code Online (Sandbox Code Playgroud)

这种方法的缺点是每个调用站点都必须指定 id。这意味着您无法使用内置的便捷方法,例如GetStringAsync; 如果这样做,将会抛出上面的异常。相反,您的代码必须使用较低级别的SendAsync方法:

var request = new HttpRequestMessage(HttpMethod.Get, "http://url.com");
request.SetId(id);
var response = await client.SendAsync(request);
Run Code Online (Sandbox Code Playgroud)

调用样板相当难看。您可以将其包装到您自己的 GetStringAsync便捷方法中;像这样的东西应该有效:

public static class HttpClientExtensions
{
  public static async Task<string> GetStringAsync(int id, string url)
  {
    var request = new HttpRequestMessage(HttpMethod.Get, url);
    request.SetId(id);
    var response = await client.SendAsync(request);
    return await response.Content.ReadAsStringAsync();
  }
}
Run Code Online (Sandbox Code Playgroud)

现在你的调用站点看起来又干净了:

public async Task DoEnquiry(IHttpClientFactory factory)
{
  int id = Database.InsertNewEnquiry();
  var http = factory.CreateClient();
  var result = await http.GetStringAsync(id, "http://url.com");
}
Run Code Online (Sandbox Code Playgroud)