使用状态拦截带有第三方扩展的 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<T>
。 “异步本地”有点像线程本地存储,但用于特定的异步代码块。
使用 AsyncLocal<T>
时有一些注意事项,但没有特别详细的记录:
-
为
T
使用不可变的可为空类型。
当设置异步本地值时,返回一个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 值有一个明确的范围。该范围内的任何代码都可以通过调用AmbientContext.TryGetId
来获取id。使用此模式可确保 any 代码都是如此:同步、async
、ConfigureAwait(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<T>
存在之前编写,但有其工作原理的详细信息。这回答了“为什么T
应该是不可变的?”的问题。和“如果我不使用IDisposable
,为什么我必须从async
方法中设置值?”。
【讨论】:
太棒了。您是否认为所有这些(IHttpClientFactory、自定义 DelgatingHandlers 和本地存储)都比旧时尚WebRequest.Create(uri)
更好,拥有自己的实例化、自己的处理程序等?
@WillemBijker:找出所有部分可能很困难,但是一旦将所有部分放在一起,我认为这是一个更简洁的 API。以上是关于使用状态拦截带有第三方扩展的 HttpClient的主要内容,如果未能解决你的问题,请参考以下文章
来自 Azure 函数的 C# HttpClient POST 请求,带有用于第三方 API 的授权标记,被剥离了标头和正文