簡體   English   中英

使用 state 通過第三方擴展攔截 HttpClient

[英]Intercept HttpClient with third party extensions using state

在使用 IHttpClientFactory 時將 state 注入 HttpRequest 可以通過填充HttpRequestMessage.Properties來實現。請參閱使用 DelegatingHandler 和 HttpClient 上的自定義數據

現在,如果我在 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
}

我目前唯一可行的解決方案是覆蓋 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 客戶端的基礎 class。 一個示例是 FacebookHttpClient 提供特定於 Facebook web 服務的附加方法(例如 GetFriends 方法)。 派生類不應覆蓋 class 上的虛擬方法。 相反,使用接受 HttpMessageHandler 的構造函數重載來配置任何請求前或請求后處理。

我幾乎將此作為替代解決方案包含在我的其他答案中,但我認為它已經太長了。 :)

該技術實際上是相同的,但不是HttpRequestMessage.Properties ,而是使用AsyncLocal<T> “異步本地”有點像線程本地存儲,但用於特定的異步代碼塊。

使用AsyncLocal<T>有一些注意事項,這些注意事項並沒有特別詳細的記錄:

  1. T使用不可變的可為空類型。
  2. 設置異步本地值時,返回重置它的IDisposable
    • 如果您不這樣做,則僅從async方法設置異步本地值。

不必遵循這些准則,但它們會讓您的生活更輕松。

除此之外,該解決方案類似於上一個解決方案,只是它只是使用AsyncLocal<T>代替。 從輔助方法開始:

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 值有一個明確的scope scope 中的任何代碼都可以通過調用AmbientContext.TryGetId來獲取 id。 使用此模式可確保對於任何代碼都是如此:同步、 asyncConfigureAwait(false)等等 - scope 中的所有代碼都可以獲得 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);
  }
}

后續閱讀:

  • 關於“異步本地”的博客文章- 在AsyncLocal<T>存在之前編寫,但詳細說明了它的工作原理。 這回答了“為什么T應該是不可變的?”的問題。 和“如果我不使用IDisposable ,為什么我必須從async方法設置值?”。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM