不借助HttpRequestMessage,如何将ILogger<T>传入Polly重试策略
解决方法:将调用站点的ILogger传递给Polly重试委托(针对GetStreamAsync等无HttpRequestMessage参数的HttpClient方法)
由于GetStreamAsync、GetStringAsync这类HttpClient便捷方法内部封装了HttpRequestMessage的创建,但未暴露传入Polly Context的入口,我们可以通过以下两种实用方案来传递ILogger
方案一:手动构造HttpRequestMessage,复用SendAsync传递Polly Context
直接模拟GetStreamAsync的内部逻辑,手动创建请求并传入携带ILogger的Polly Context:
调用站点代码(控制器中)
private readonly HttpClient _httpClient; private readonly ILogger<MyController> _logger; public MyController(HttpClient httpClient, ILogger<MyController> logger) { _httpClient = httpClient; _logger = logger; } public async Task<IActionResult> FetchDataStream() { // 1. 构造请求 var request = new HttpRequestMessage(HttpMethod.Get, "https://api.example.com/data-stream"); // 2. 创建Polly Context并存入ILogger var pollyContext = new Context(); pollyContext["CallSiteLogger"] = _logger; // 3. 携带Context调用SendAsync var response = await _httpClient.SendAsync(request, pollyContext); response.EnsureSuccessStatusCode(); // 4. 获取Stream(和GetStreamAsync逻辑一致) var stream = await response.Content.ReadAsStreamAsync(); // 后续处理stream... return File(stream, "application/octet-stream"); }
重试策略定义
从Polly Context中取出Logger并记录重试日志:
var retryPolicy = Policy<HttpResponseMessage> .HandleResult(resp => !resp.IsSuccessStatusCode) .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), onRetry: (result, waitTime, retryCount, context) => { // 安全取出ILogger if (context.TryGetValue("CallSiteLogger", out var loggerObj) && loggerObj is ILogger logger) { logger.LogWarning("请求失败(状态码:{StatusCode}),第{RetryCount}次重试,等待{WaitSeconds:F1}秒", result.Result.StatusCode, retryCount, waitTime.TotalSeconds); } });
方案二:扩展HttpClient,添加带Polly Context的便捷方法重载
如果需要频繁使用GetStreamAsync这类方法,可以封装扩展方法,避免重复构造HttpRequestMessage:
扩展方法定义
public static class HttpClientPollyExtensions { // 对应GetStreamAsync(string requestUri)的重载 public static async Task<Stream> GetStreamAsync(this HttpClient client, string requestUri, Context pollyContext, CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(HttpMethod.Get, requestUri); var response = await client.SendAsync(request, pollyContext, cancellationToken); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStreamAsync(cancellationToken); } // 对应GetStreamAsync(Uri requestUri)的重载 public static async Task<Stream> GetStreamAsync(this HttpClient client, Uri requestUri, Context pollyContext, CancellationToken cancellationToken = default) { using var request = new HttpRequestMessage(HttpMethod.Get, requestUri); var response = await client.SendAsync(request, pollyContext, cancellationToken); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStreamAsync(cancellationToken); } }
调用站点代码
public async Task<IActionResult> FetchDataStream() { var pollyContext = new Context(); pollyContext["CallSiteLogger"] = _logger; // 直接调用扩展方法 var stream = await _httpClient.GetStreamAsync("https://api.example.com/data-stream", pollyContext); // 后续处理stream... return File(stream, "application/octet-stream"); }
优化建议:类型安全的Context键
为避免字符串键拼写错误,可定义静态类统一管理Context键:
public static class PollyContextKeys { public const string CallSiteLogger = nameof(CallSiteLogger); }
使用时直接替换为PollyContextKeys.CallSiteLogger即可。
内容的提问来源于stack exchange,提问作者Dr. Strangelove
相关产品推荐
相关产品推荐

