[英]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>
有一些注意事项,这些注意事项并没有特别详细的记录:
T
.T
使用不可变的可为空类型。IDisposable
that resets it.IDisposable
。
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.使用此模式可确保对于任何代码都是如此:同步、
async
、 ConfigureAwait(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:后续阅读:
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.