使用状态拦截HttpClient与第三方扩展 [英] Intercept HttpClient with third party extensions using state

查看:38
本文介绍了使用状态拦截HttpClient与第三方扩展的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

使用IHttpClientFactory时,可以通过填充 HttpRequestMessage.Properties 将状态注入到HttpRequest中,请参见

Injecting state into your HttpRequest when using IHttpClientFactory is achievable by populating HttpRequestMessage.Properties see Using DelegatingHandler with custom data on HttpClient

现在,如果我在HttpClient上具有第三方扩展名(例如IdentityModel),我将如何使用自定义状态拦截这些HTTP请求?

Now if I have third party extensions on HttpClient (such as IdentityModel), how would I intercept these http requests using custom state?

public async Task DoEnquiry(IHttpClientFactory factory)
{
    var id = Database.InsertEnquiry();
    var httpClient = factory.CreateClient();
    // GetDiscoveryDocumentAsync is a third party extension method on HttpClient
    // I therefore cannot inject or alter the request message to be handled by the InterceptorHandler
    var discovery = await httpClient.GetDiscoveryDocumentAsync();
    // I want id to be associated with any request / response GetDiscoveryDocumentAsync is making
}

我目前唯一可行的解​​决方案是重写HttpClient.

The only plausible solution I currently have is to override HttpClient.

public class InspectorHttpClient: HttpClient
{
    
    private readonly HttpClient _internal;
    private readonly int _id;

    public const string Key = "insepctor";

    public InspectorHttpClient(HttpClient @internal, int id)
    {
        _internal = @internal;
        _id = id;
    }
    
    public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // attach data into HttpRequestMessage for the delegate handler
        request.Properties.Add(Key, _id);
        return _internal.SendAsync(request, cancellationToken);
    }

    // override all methods forwarding to _internal
}

然后我就可以拦截这些请求.

A then I'm able to intercept these requests.

public async Task DoEnquiry(IHttpClientFactory factory)
{
    var id = Database.InsertEnquiry();
    var httpClient = new InspectorHttpClient(factory.CreateClient(), id);
    var discovery = await httpClient.GetDiscoveryDocumentAsync();
}

这是一个可行的解决方案吗?现在有件事告诉我不要覆盖HttpClient.从 https://引用docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-5.0

Is that a plausible solution? Something tell me now not to override HttpClient. Quoting from https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-5.0

HttpClient还充当更特定的HTTP客户端的基类.一个示例是提供特定于Facebook Web服务的其他方法的FacebookHttpClient(例如,GetFriends方法).派生类不应覆盖类上的虚方法.而是使用接受HttpMessageHandler的构造函数重载来配置任何请求前或请求后处理.

The HttpClient also acts as a base class for more specific HTTP clients. An example would be a FacebookHttpClient providing additional methods specific to a Facebook web service (a GetFriends method, for instance). Derived classes should not override the virtual methods on the class. Instead, use a constructor overload that accepts HttpMessageHandler to configure any pre- or post-request processing instead.

推荐答案

我几乎将此内容包含在我的其他答案中,作为另一种解决方案,但我认为这已经太久了.:)

I almost included this in my other answer as an alternative solution, but I figured it was too long already. :)

该技术实际上是相同的,但是使用 AsyncLocal< T> 代替 HttpRequestMessage.Properties .本地异步"有点像线程本地存储,但是用于特定的异步代码块.

The technique is practically the same, but instead of HttpRequestMessage.Properties, use AsyncLocal<T>. "Async local" is kind of like thread-local storage but for a specific asynchronous code block.

使用 AsyncLocal< T> 的一些注意事项并没有特别有据可查:

There are a few caveats to using AsyncLocal<T> that aren't particularly well-documented:

  1. T 使用不可变的可空类型.
  2. 设置异步本地值时,返回一个 IDisposable 对其进行重置.
    • 如果不这样做,则只能通过 async 方法设置异步本地值.
  1. Use an immutable nullable type for T.
  2. When setting the async local value, return an IDisposable that resets it.
    • If you don't do this, then only set the async local value from an async method.

没有要遵守这些准则,但是它们会使您的生活更加轻松.

You don't have to follow these guidelines, but they will make your life much easier.

通过这种方式,解决方案与上一个类似,不同之处在于它只使用了 AsyncLocal< T> .从助手方法开始:

With that out of the way, the solution is similar to the last one, except it just uses AsyncLocal<T> instead. Starting with the helper methods:

public static class AmbientContext
{
  public static IDisposable SetId(int id)
  {
    var oldValue = AmbientId.Value;
    AmbientId.Value = id;
    // The following line uses Nito.Disposables; feel free to write your own.
    return Disposable.Create(() => AmbientId.Value = oldValue);
  }

  public static int? TryGetId() => AmbientId.Value;

  private static readonly AsyncLocal<int?> AmbientId = new AsyncLocal<int?>();
}

然后将更新调用代码以设置环境值:

Then the calling code is updated to set the ambient value:

public async Task DoEnquiry(IHttpClientFactory factory)
{
  var id = Database.InsertEnquiry();
  using (AmbientContext.SetId(id))
  {
    var httpClient = factory.CreateClient();
    var discovery = await httpClient.GetDiscoveryDocumentAsync();
  }
}

请注意,该环境ID值有一个明确的 scope .该范围内的任何代码都可以通过调用 AmbientContext.TryGetId 来获取ID.使用此模式可确保对任何代码都是正确的:同步, async ConfigureAwait(false),无论如何-该范围内的所有代码都可以获取id值.包括您的自定义处理程序:

Note that there is an explicit scope for that ambient id value. Any code within that scope can get the id by calling AmbientContext.TryGetId. Using this pattern ensures that this is true for any code: synchronous, async, ConfigureAwait(false), whatever - all code within that scope can get the id value. Including your custom handler:

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

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

后续阅读:

  • 异步本地"博客上的帖子-写在 AsyncLocal< T> 存在之前,但详细说明了其工作原理.这回答了为什么 T 是不可变的?"的问题.和如果我不使用 IDisposable ,为什么我必须通过 async 方法设置值?".
  • Blog post on "async local" - written before AsyncLocal<T> existed, but has details on how it works. This answers the questions "why should T be immutable?" and "if I don't use IDisposable, why do I have to set the value from an async method?".

这篇关于使用状态拦截HttpClient与第三方扩展的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆