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

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

问题描述

在使用 IHttpClientFactory 时将状态注入您的 HttpRequest 可以通过填充 HttpRequestMessage.Properties 来实现,参见 将 DelegatingHandler 与 HttpClient 上的自定义数据一起使用

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 客户端的基类.一个示例是 FacebookHttpClient 提供特定于 Facebook Web 服务的附加方法(例如 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 而不是 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 代替.从辅助方法开始:

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 值有一个明确的范围.该范围内的任何代码都可以通过调用 AmbientContext.TryGetId 来获取 id.使用此模式可确保对于任何代码都是如此:同步、asyncConfigureAwait(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 应该是不可变的?"的问题.以及如果我不使用 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天全站免登陆