使用状态拦截带有第三方扩展的 HttpClient
Intercept HttpClient with third party extensions using state
在使用 IHttpClientFactory 时将状态注入 HttpRequest 可以通过填充 HttpRequestMessage.Properties
来实现,请参阅
现在,如果我在 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
}
然后我就可以拦截这些请求了。
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
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.
我差点把它作为替代解决方案包含在 中,但我认为它已经太长了。 :)
该技术实际上是相同的,但不是 HttpRequestMessage.Properties
,而是使用 AsyncLocal<T>
。 “异步本地”有点像线程本地存储,但用于特定的异步代码块。
有一些使用 AsyncLocal<T>
的注意事项,这些注意事项并没有特别详细地记录:
- 为
T
使用不可变的可空类型。
- 设置异步本地值时,return 一个
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 时将状态注入 HttpRequest 可以通过填充 HttpRequestMessage.Properties
来实现,请参阅
现在,如果我在 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
}
然后我就可以拦截这些请求了。
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
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.
我差点把它作为替代解决方案包含在
该技术实际上是相同的,但不是 HttpRequestMessage.Properties
,而是使用 AsyncLocal<T>
。 “异步本地”有点像线程本地存储,但用于特定的异步代码块。
有一些使用 AsyncLocal<T>
的注意事项,这些注意事项并没有特别详细地记录:
- 为
T
使用不可变的可空类型。 - 设置异步本地值时,return 一个
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
方法设置值?”。