简体   繁体   English

使用 state 通过第三方扩展拦截 HttpClient

[英]Intercept HttpClient with third party extensions using state

Injecting state into your HttpRequest when using IHttpClientFactory is achievable by populating HttpRequestMessage.Properties see Using DelegatingHandler with custom data on HttpClient在使用 IHttpClientFactory 时将 state 注入 HttpRequest 可以通过填充HttpRequestMessage.Properties来实现。请参阅使用 DelegatingHandler 和 HttpClient 上的自定义数据

Now if I have third party extensions on HttpClient (such as IdentityModel), how would I intercept these http requests using custom state?现在,如果我在 HttpClient 上有第三方扩展(例如 IdentityModel),我将如何使用自定义 state 拦截这些 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
}

The only plausible solution I currently have is to override HttpClient.我目前唯一可行的解决方案是覆盖 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. A 然后我可以拦截这些请求。

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

Is that a plausible solution?这是一个合理的解决方案吗? Something tell me now not to override HttpClient.现在告诉我不要覆盖 HttpClient。 Quoting from https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-5.0引自https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-5.0

The HttpClient also acts as a base class for more specific HTTP clients. HttpClient 还充当更具体的 HTTP 客户端的基础 class。 An example would be a FacebookHttpClient providing additional methods specific to a Facebook web service (a GetFriends method, for instance).一个示例是 FacebookHttpClient 提供特定于 Facebook web 服务的附加方法(例如 GetFriends 方法)。 Derived classes should not override the virtual methods on the class.派生类不应覆盖 class 上的虚拟方法。 Instead, use a constructor overload that accepts HttpMessageHandler to configure any pre- or post-request processing instead.相反,使用接受 HttpMessageHandler 的构造函数重载来配置任何请求前或请求后处理。

I almost included this in my other answer as an alternative solution, but I figured it was too long already.我几乎将此作为替代解决方案包含在我的其他答案中,但我认为它已经太长了。 :) :)

The technique is practically the same, but instead of HttpRequestMessage.Properties , use AsyncLocal<T> .该技术实际上是相同的,但不是HttpRequestMessage.Properties ,而是使用AsyncLocal<T> "Async local" is kind of like thread-local storage but for a specific asynchronous code block. “异步本地”有点像线程本地存储,但用于特定的异步代码块。

There are a few caveats to using AsyncLocal<T> that aren't particularly well-documented:使用AsyncLocal<T>有一些注意事项,这些注意事项并没有特别详细的记录:

  1. Use an immutable nullable type for T .T使用不可变的可为空类型。
  2. When setting the async local value, return an IDisposable that resets it.设置异步本地值时,返回重置它的IDisposable
    • If you don't do this, then only set the async local value from an async method.如果您不这样做,则仅从async方法设置异步本地值。

You don't have to follow these guidelines, but they will make your life much easier.不必遵循这些准则,但它们会让您的生活更轻松。

With that out of the way, the solution is similar to the last one, except it just uses AsyncLocal<T> instead.除此之外,该解决方案类似于上一个解决方案,只是它只是使用AsyncLocal<T>代替。 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();
  }
}

Note that there is an explicit scope for that ambient id value.请注意,该环境 id 值有一个明确的scope Any code within that scope can get the id by calling AmbientContext.TryGetId . scope 中的任何代码都可以通过调用AmbientContext.TryGetId来获取 id。 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.使用此模式可确保对于任何代码都是如此:同步、 asyncConfigureAwait(false)等等 - scope 中的所有代码都可以获得 id 值。 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);
  }
}

Followup readings:后续阅读:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM