在 HttpClient 上使用带有自定义数据的 DelegatingHandler
Using DelegatingHandler with custom data on HttpClient
鉴于众所周知的使用 HttpClient 的困境和问题 - 即套接字耗尽和不尊重 DNS 更新,它被认为是使用 IHttpClientFactory 并让容器决定何时以及如何利用 http 池连接效率的最佳实践。这一切都很好,但现在我无法在每个请求上使用自定义数据实例化自定义 DelegatingHandler。
下面的示例说明了我在使用工厂方法之前是如何做到的:
public class HttpClientInterceptor : DelegatingHandler
{
private readonly int _id;
public HttpClientInterceptor(int id)
{
_id = id;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// associate the id with this request
Database.InsertEnquiry(_id, request);
return await base.SendAsync(request, cancellationToken);
}
}
每次我实例化一个 HttpClient 时,都可以传递一个 Id:
public void DoEnquiry()
{
// Insert a new enquiry hypothetically
int id = Database.InsertNewEnquiry();
using (var http = new HttpClient(new HttpClientInterceptor(id)))
{
// and do some operations on the http client
// which will be logged in the database associated with id
http.GetStringAsync("http://url.com");
}
}
但现在我无法实例化 HttpClient 或 Handlers。
public void DoEnquiry(IHttpClientFactory factory)
{
int id = Database.InsertNewEnquiry();
var http = factory.CreateClient();
// and now??
http.GetStringAsync("http://url.com");
}
我怎样才能使用工厂实现类似的功能?
I cannot instantiate a custom DelegatingHandler with custom data on each request.
这是正确的。但是您可以使用可重用(且无状态)的自定义 DelegatingHandler
,并将数据作为请求的一部分传递。这就是 HttpRequestMessage.Properties
的用途。在进行这些类型的“自定义上下文”操作时,我更喜欢将我的上下文“属性”定义为扩展方法:
public static class HttpRequestExtensions
{
public static HttpRequestMessage WithId(this HttpRequestMessage request, int id)
{
request.Properties[IdKey] = id;
return request;
}
public static int? TryGetId(this HttpRequestMessage request)
{
if (request.Properties.TryGetValue(IdKey, out var value))
return value as int?;
return null;
}
private static readonly string IdKey = Guid.NewGuid().ToString("N");
}
然后你可以在(可重复使用的)DelegatingHandler
:
public class HttpClientInterceptor : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var id = request.TryGetId();
if (id == null)
throw new InvalidOperationException("This request must have an id set.");
// associate the id with this request
Database.InsertEnquiry(id.Value, request);
return await base.SendAsync(request, cancellationToken);
}
}
这种方法的缺点是每个调用点都必须指定 id。这意味着您不能使用像 GetStringAsync
这样的内置便捷方法;如果你这样做,上面的异常将被抛出。相反,您的代码将不得不使用较低级别的 SendAsync
方法:
var request = new HttpRequestMessage(HttpMethod.Get, "http://url.com");
request.SetId(id);
var response = await client.SendAsync(request);
调用样板相当难看。您可以将其包装到您的 own GetStringAsync
便捷方法中;这样的事情应该有效:
public static class HttpClientExtensions
{
public static async Task<string> GetStringAsync(int id, string url)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.SetId(id);
var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
}
现在您的呼叫站点再次看起来更干净了:
public async Task DoEnquiry(IHttpClientFactory factory)
{
int id = Database.InsertNewEnquiry();
var http = factory.CreateClient();
var result = await http.GetStringAsync(id, "http://url.com");
}
鉴于众所周知的使用 HttpClient 的困境和问题 - 即套接字耗尽和不尊重 DNS 更新,它被认为是使用 IHttpClientFactory 并让容器决定何时以及如何利用 http 池连接效率的最佳实践。这一切都很好,但现在我无法在每个请求上使用自定义数据实例化自定义 DelegatingHandler。
下面的示例说明了我在使用工厂方法之前是如何做到的:
public class HttpClientInterceptor : DelegatingHandler
{
private readonly int _id;
public HttpClientInterceptor(int id)
{
_id = id;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// associate the id with this request
Database.InsertEnquiry(_id, request);
return await base.SendAsync(request, cancellationToken);
}
}
每次我实例化一个 HttpClient 时,都可以传递一个 Id:
public void DoEnquiry()
{
// Insert a new enquiry hypothetically
int id = Database.InsertNewEnquiry();
using (var http = new HttpClient(new HttpClientInterceptor(id)))
{
// and do some operations on the http client
// which will be logged in the database associated with id
http.GetStringAsync("http://url.com");
}
}
但现在我无法实例化 HttpClient 或 Handlers。
public void DoEnquiry(IHttpClientFactory factory)
{
int id = Database.InsertNewEnquiry();
var http = factory.CreateClient();
// and now??
http.GetStringAsync("http://url.com");
}
我怎样才能使用工厂实现类似的功能?
I cannot instantiate a custom DelegatingHandler with custom data on each request.
这是正确的。但是您可以使用可重用(且无状态)的自定义 DelegatingHandler
,并将数据作为请求的一部分传递。这就是 HttpRequestMessage.Properties
的用途。在进行这些类型的“自定义上下文”操作时,我更喜欢将我的上下文“属性”定义为扩展方法:
public static class HttpRequestExtensions
{
public static HttpRequestMessage WithId(this HttpRequestMessage request, int id)
{
request.Properties[IdKey] = id;
return request;
}
public static int? TryGetId(this HttpRequestMessage request)
{
if (request.Properties.TryGetValue(IdKey, out var value))
return value as int?;
return null;
}
private static readonly string IdKey = Guid.NewGuid().ToString("N");
}
然后你可以在(可重复使用的)DelegatingHandler
:
public class HttpClientInterceptor : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var id = request.TryGetId();
if (id == null)
throw new InvalidOperationException("This request must have an id set.");
// associate the id with this request
Database.InsertEnquiry(id.Value, request);
return await base.SendAsync(request, cancellationToken);
}
}
这种方法的缺点是每个调用点都必须指定 id。这意味着您不能使用像 GetStringAsync
这样的内置便捷方法;如果你这样做,上面的异常将被抛出。相反,您的代码将不得不使用较低级别的 SendAsync
方法:
var request = new HttpRequestMessage(HttpMethod.Get, "http://url.com");
request.SetId(id);
var response = await client.SendAsync(request);
调用样板相当难看。您可以将其包装到您的 own GetStringAsync
便捷方法中;这样的事情应该有效:
public static class HttpClientExtensions
{
public static async Task<string> GetStringAsync(int id, string url)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.SetId(id);
var response = await client.SendAsync(request);
return await response.Content.ReadAsStringAsync();
}
}
现在您的呼叫站点再次看起来更干净了:
public async Task DoEnquiry(IHttpClientFactory factory)
{
int id = Database.InsertNewEnquiry();
var http = factory.CreateClient();
var result = await http.GetStringAsync(id, "http://url.com");
}