使用状态拦截带有第三方扩展的 HttpClient

Posted

技术标签:

【中文标题】使用状态拦截带有第三方扩展的 HttpClient【英文标题】:Intercept HttpClient with third party extensions using state 【发布时间】:2021-02-19 10:55:01 【问题描述】:

在使用 IHttpClientFactory 时将状态注入到您的 HttpRequest 可以通过填充 HttpRequestMessage.Properties 来实现,参见 Using DelegatingHandler with custom data on HttpClient

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

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。

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 那么我就可以拦截这些请求了。

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

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

【问题讨论】:

【参考方案1】:

我几乎将它作为替代解决方案包含在my other answer 中,但我认为它已经太长了。 :)

技术实际上是相同的,但不是HttpRequestMessage.Properties,而是使用AsyncLocal&lt;T&gt;。 “异步本地”有点像线程本地存储,但用于特定的异步代码块。

使用 AsyncLocal&lt;T&gt; 时有一些注意事项,但没有特别详细的记录:

    T 使用不可变的可为空类型。 当设置异步本地值时,返回一个IDisposable 来重置它。 如果您不这样做,则仅通过 async 方法设置异步本地值。

您不必遵循这些准则,但它们会让您的生活更轻松。

除此之外,解决方案与上一个类似,只是它只使用AsyncLocal&lt;T&gt;。从辅助方法开始:

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?>();

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

public async Task DoEnquiry(IHttpClientFactory factory)

  var id = Database.InsertEnquiry();
  using (AmbientContext.SetId(id))
  
    var httpClient = factory.CreateClient();
    var discovery = await httpClient.GetDiscoveryDocumentAsync();
  

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

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

后续读数:

Blog post on "async local" - 在 AsyncLocal&lt;T&gt; 存在之前编写,但有其工作原理的详细信息。这回答了“为什么T 应该是不可变的?”的问题。和“如果我不使用IDisposable,为什么我必须从async 方法中设置值?”。

【讨论】:

太棒了。您是否认为所有这些(IHttpClientFactory、自定义 DelgatingHandlers 和本地存储)都比旧时尚 WebRequest.Create(uri) 更好,拥有自己的实例化、自己的处理程序等? @WillemBijker:找出所有部分可能很困难,但是一旦将所有部分放在一起,我认为这是一个更简洁的 API。

以上是关于使用状态拦截带有第三方扩展的 HttpClient的主要内容,如果未能解决你的问题,请参考以下文章

带有客户端证书的 HttpClient POST 请求

来自 Azure 函数的 C# HttpClient POST 请求,带有用于第三方 API 的授权标记,被剥离了标头和正文

如何使用httpclient拦截角度jsonp请求

如何将异步服务用于角度 httpClient 拦截器

关于HttpClient重试策略的研究

HttpClient 发送 HTTPHTTPS 请求的简单封装