在 ASP.NET Core 2.1 之后与 HttpClient 工厂一起使用 Polly

在 ASP.NET Core 2.1 之后与 HttpClient 工厂一起使用 Polly

在 ASP.NET Core 2.1 中提供的 HttpClient factory 提供了一种预配置 HttpClient 的方式,可以应用 Polly 到任何对外部的调用中。

注:如果你在与 HttpClient 一起使用 Polly 7 时,遇到钻石依赖问题,请按照此方案处理。

当前版本:7.2.1

什么是 HttpClientFactory?

从 ASPNET Core 2.1 开始,Polly 开始与 IHttpClientFactory 进行集成。 HttpClient factory 是一个可以简化管理和使用 HttpClient 的工厂,它通过如下四种方式:

  • 支持命名并配置逻辑的 HttpClient。例如,你可以配置一个预先配置好的用来访问 GitHub API 的 HttpClient.
  • 管理 HttpClientMessageHandler 的生命周期,以避免你自己管理 HttpClient 相关的问题。频繁地丢弃导致的 socket 耗尽,但是仅仅使用一个 HttpClient 实例却导致 DNS 更新丢失问题。
  • 通过 ILogger 提供可配置的日志,对通过工厂创建的 HttpClient 进行的所有请求和资源访问执行记录。
  • 支持简单的 API 来添加对外请求和响应的中间件,支持日志,授权,服务发现,或者基于 Polly 的弹性访问

微软以前发布过很多关于此方面的说明Steve Gordon 的四篇博客 (1; 2; 3; 4) 对于深入的背景也非常优秀,并提供了一些很棒的可以使用的示例。更新:官方文档 现在也发布了。

与 IHttpClientFactory 一起使用 Polly

步骤 1: 在项目中引用 ASP.NET Core 2.1 (或更新的版本) 包和 Microsoft.Extensions.Http.Polly

如果你使用的是 ASP.NET Core 5,以及 Microsoft.Extensions.Http.Polly。那么你的项目文件看起来如下所示:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Polly" Version="7.2.1" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
  </ItemGroup>

</Project>

⚠️ 当你阅读本文的时候,后继版本的包可能已经发布了。

步骤 2:在 Startup 文件中使用 Polly 策略配置 HttpClient
定义命名的 HttpClient

在标准的 Startup.ConfigureServices(......) 方法中,如下所示配置命名的 HttpClient:

public void ConfigureServices(IServiceCollection services)
{
    // Configure a client named as "GitHub", with various default properties.
    services.AddHttpClient("GitHub", client =>
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        client.DefaultRequestHeaders.Add("User-Agent", "HttpClient");
    });

    // ...
}

为了清晰起见,这里使用了固定的字符串,当然你应该从配置文件或者使用常量来声明它们

我们将专注于使用策略进行配置,但是这里有很多的配置选项可以用来配置命名的 HttpClient,你可以从官方文档 中查看详细内容,或者从 Steve GordonScott Hanselman 的博客。

为了使得本文中的示例简洁,我们将使用命名的客户端,但是上面的文档和博客都是关于使用类型化的客户端,这样提供了强类型的优势,并支持你在类型化客户端上进行扩展来专注于你的特定需求。

使用 Polly 策略扩展来进行客户端的配置

为了应用 Polly 策略,你可以简单地使用流畅方式进行配置来扩展上述示例。

services.AddHttpClient("GitHub", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
})
.AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
{
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(5),
    TimeSpan.FromSeconds(10)
}));

该示例创建了一个策略,它将处理典型的瞬时失败,如果需要的话,重试底层的 Http 请求 3 次。该策略将使在首次失败之后延迟 1 秒,第二次重试之后延迟 5 秒,第三次失败之后延迟 10 秒。

扩展方法 .AddTransientHttpErrorPolicy(...) 是 Polly 提供的众多选项之一,在入门之后我们将看到它们。

步骤 3:使用配置的 HttpClient

最后,这里是使用预先配置的 HttpClient 的示例。对于命名的客户端 (上述示例)。通过依赖注入获得一个 IHttpClientFactory,然后使用该工厂获取在 Startup 中预先配置的 HttpClient。

public class MyController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;

    public MyController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    
    public Task<IActionResult> SomeAction()
    {
        // Get an HttpClient configured to the specification you defined in StartUp.
        var client = _httpClientFactory.CreateClient("GitHub"); 
    
        return Ok(await client.GetStringAsync("/someapi"));
    }
}

代码中的调用 await client.GetStringAsync("/someapi") 将在调用中应用预配置的策略,如下节所述。

再强调一下, Steve Gordon's and Scott Hanselman 的博客提供了更丰富的示例,包括你可能更期望的强类型客户端。

Polly 策略是如何应用的?

对你的 HttpClient 配置的策略或者策略组通过基于策略的 DelegatingHandler 应用于对外的请求中。

这意味着策略将通过预配置的 HttpClient 应用于所有对外的请求中。

如果你以前尝试过手工重试过通过传递一个 HttpRequestMessage 对外调用 HttpClient.SendAsync(...),你可能已经知道 HttpRequestMessage 一旦被发送使用就不能被重用 (会发出一个 InvalidOperationException 异常)。而 DelegatingHandler 可以规避此问题。

一个 DelegatingHandler 对于对外调用的 Http 来说是简单的中间件:请参阅 Steve Gordon 的 3 篇博客 来获得委托处理器是如何工作的介绍。

DelegatingHandler 对于 Http 对外请求来说是简单的中间件:

配置 Polly 策略

使用 .AddTransientHttpErrorPolicy(...)

我们再回顾一下步骤 2 的示例:

services.AddHttpClient("GitHub", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
})
.AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
{
    TimeSpan.FromSeconds(1),
    TimeSpan.FromSeconds(5),
    TimeSpan.FromSeconds(10)
}));

这里使用了一个便捷的方法 .AddTransientHttpErrorPolicy(...)。它配置了一个典型的处理 Http 调用错误的策略:

  • Network failures (System.Net.Http.HttpRequestException)
  • HTTP 5XX status codes (server errors)
  • HTTP 408 status code (request timeout)

使用 .AddTransientHttpErrorPolicy(...) 预先配置了策略要处理的内容。该 builder => builder 子句然后定义了策略如何处理这些失效。

builder => builder 子句中,你可以从 Polly 提供的功能中选择任何重新激活策略:重试策略 (如上所示),断路器或者回退策略。

在 .AddTransientHttpErrorPolicy(...) 中选择处理 HttpRequestException, HTTP 5xx, HTTP 408 是通常的选择,但是不是强制的。如果这些错误过滤器不能满足你的需求,那些你想到的,你可以扩展需要处理的错误定义,或者构建一个完整的订制策略。

通过经典的 Polly 语法使用任何配置的策略

通过 IAsyncPolicy<HttpResponseMessage> 的扩展也一样存在,所以你可以定义和应用任何类型的策略:通过指定需要处理什么和如何处理两者。

下面的示例演示了通过 .AddPolicyHandler(...) 来增加我们编码的策略来处理自己指定的失败。

var retryPolicy = Policy.Handle<HttpRequestException>()
    .OrResult<HttpResponseMessage>(response => MyCustomResponsePredicate(response))
    .WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    }));

services.AddHttpClient(/* etc */)
    .AddPolicyHandler(retryPolicy);

与使用 Polly 的重激活策略一样 (例如重试和断路器),这些扩展意味着你也可以使用主动激活策略 例如超时。

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10);

services.AddHttpClient(/* etc */)
    .AddPolicyHandler(timeoutPolicy);

所有通过 HttpClient 进行的调用都返回 HttpResponseMessage,所以配置的策略类型必须是 IAsyncPolicy<HttpResponseMessage> 类型。非范型的策略 IAsyncPolicy 也可以使用方便的方法转化为 IAsyncPolicy<HttpResponseMessage> 。

var timeoutPolicy = Policy.TimeoutAsync(10);

services.AddHttpClient(/* etc */)
    .AddPolicyHandler(timeoutPolicy.AsAsyncPolicy<HttpResponseMessage>());
扩展方便的 .AddTransientHttpErrorPolicy(...) 定义

通过 .AddTransientHttpErrorPolicy(...) 来处理预定义的错误,还可以通过 Polly 扩展包 Polly.Extensions.Http 来实现。

使用它,你可以在基本的错误处理基础之上 ( HttpRequestException, HTTP 5xx, HTTP 408 ),进行扩展。

例如,下面配置的策略还处理了状态码 429

using Polly.Extensions.Http; // After installing the nuget package: Polly.Extensions.Http

// ..

var policy = HttpPolicyExtensions
  .HandleTransientHttpError() // HttpRequestException, 5XX and 408
  .OrResult(response => (int)response.StatusCode == 429) // RetryAfter
  .WaitAndRetryAsync(/* etc */);
应用多个策略

所有用来配置策略的扩展也可以使用链式来应用多种策略:

services.AddHttpClient(/* etc */)
    .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    }))
    .AddTransientHttpErrorPolicy(builder => builder.CircuitBreakerAsync(
        handledEventsAllowedBeforeBreaking: 3,
        durationOfBreak: TimeSpan.FromSeconds(30)
    ));
多个策略的应用顺序

在配置多个策略的时候,比如上面的示例中。从外 (前面的配置) 向里 (后面的配置) 应用策略:

在上述示例中,调用将如下发生:

  • 首先执行外层的重试策略:
  • 然后内层的断路器策略:
  • 执行底层的 HTTP 调用.

在本示例中的策略顺序是因为当重试策略在重试之间等待的时候,断路器策略可能被改变了状态。断路器被配置在重试策略内部,导致断路器被作为重试操作的一部分被重复检测。

上面的示例应用了 2 个策略 (重试和断路器),但是可以使用任意数目。常见的组合可能应用了重试,断路器,以及每次的超时。

与 PollyWrap 相比

这些与 Polly 的 PolicyWrap 类似,使用上述方式配置的多个策略与使用 PollyWrap 是等效的。在 PollyWrap wiki 中建议的使用方式

类似的,如果你与其它的 DelegatingHandler 组合使用 PolicyHttpMessageHandler,考虑不论策略处理器应该在你构建的中间件管线中的其它委托处理器内部或者外部。,应用的 DelegatingHandler 被应用的顺序与你在 .AddHttpClient( /* etc */ ) 调用之后配置的顺序相关。

动态选择策略

扩展 .AddPolicyHandler(...) 允许你根据请求动态选择策略。

一个案例是根据非等幂应用不一样的行为策略。POST 操作是典型的非等幂操作。PUT 操作应该是幂等的,但是对于特定的 API 可能不是这样 (对于你调用的 API 知道的替代行为)。所以,你可能希望定义一个策略,它可以对 GET 请求进行重试,但是对其它 HTTP 请求则不重试:

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    });
var noOpPolicy = Policy.NoOpAsync().AsAsyncPolicy<HttpResponseMessage>();

services.AddHttpClient(/* etc */)
    // Select a policy based on the request: retry for Get requests, noOp for other http verbs.
    .AddPolicyHandler(request => request.Method == HttpMethod.Get ? retryPolicy : noOpPolicy);

上述的示例代码中,对 GET 之外的动词使用 NoOp policy 。空操作策略简单地执行底层的请求,没有任何其它的策略行为。

对于有状态的策略使用选择器,而不是工厂

当在 HttpClientFactory 上使用有状态的策略,例如断路器和隔舱,使用 .AddPolicyHandler(policySelector: request => ...) (以及类似的) 扩展时,你必须确保策略选择器不会针对每个请求创建新的实例,而是对断路器或者隔舱使用单个实例。以便于单个实例可以在多个请求之间保持状态。不要如下编码:

// BAD CODE (do not use)
 services.AddHttpClient(/* etc */)
    .AddPolicyHandler(request => /* some func sometimes returning */ 
        HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(...)) // This will manufacture a new circuit-breaker per request.

而应该使用

var circuitBreaker = HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(...);
 services.AddHttpClient(/* etc */)
    .AddPolicyHandler(request => /* some func sometimes returning */ circuitBreaker )

注意一个更高级一些的案例。如果你调用一个收到断路器保护的动态下游节点,你不能对每个下游节点预定义 (在配置时);相反,你需要在新的节点首次被使用的时候创建新的断路器 (策略工厂模式),但是在节点被使用的后继子请求中,选择前面创建的断路器 (策略选择器方式)。对于此用例的讨论参见下面的 在策略注册中使用 GetOrAdd(...) 风格方式。

从策略注册中选择策略

Polly 还提供了实用 PollyRegistry 作为中央存储的方式,你可以在应用的多个地方重用策略。扩展的 .AddPolicyHandler(...) 支持你从注册表中选择策略。

下面的示例中:

  • 创建 PolicyRegistry 并添加一些策略到其中,使用集合初始化语法
  • 使用 IServiceCollection 注册 PolicyRegistry
  • 从注册表中使用不同的策略定义逻辑的 HttpClient

代码:

var registry = new PolicyRegistry()
{
    { "defaultretrystrategy", HttpPolicyExtensions.HandleTransientHttpError().WaitAndRetryAsync(/* etc */) },
    { "defaultcircuitbreaker", HttpPolicyExtensions.HandleTransientHttpError().CircuitBreakerAsync(/* etc */) },
};

services.AddPolicyRegistry(registry);

services.AddHttpClient(/* etc */)
    .AddPolicyHandlerFromRegistry("defaultretrystrategy")
    .AddPolicyHandlerFromRegistry("defaultcircuitbreaker");

更高级的使用 PolicyRegistry 用例包括从外部来源动态更新注册表中的策略,可以参考 dynamic reconfiguration of policies during running

还有一些 more complex overloads on HttpClientFactory 允许从注册表中动态选择策略,基于 HttpRequestMessage。

对 PolicyRegistry 使用 GetOrAdd(...) 风格方式

更高级的方式是你可能希望对 PolicyRegistry 进行 .GetOrAdd(...)。如果你调用受到断路器保护的动态下游节点,你就不能对每个下游节点预先定义 (在 DI 配置的时候) 断路器。相反,你需要在下游节点首次使用的时候创建新的断路器 (策略工厂方式)。但是对于该节点的后继每次请求,选择前面生成的断路器 (策略选择方式)。对于扩展支持该场景,参见 this discussion。这里演示了对 IHttpClientFactory 的一种扩展,支持你在 PolicyRegistry 中进行存储然后提取策略 (例如断路器) ,使用 GetOrAdd(...) 风格的方式。

案例:应用超时

HttpClient 已经有一个 Timeout 属性。但是在重试策略被应用的时候它是如何被应用的呢?Polly 的 TimeoutPolicy 适合在哪里呢?

  • HttpClient.Timeout 被应用于每个整个 HttpClient 调用,包括所有的重试和重试之间的等待

  • 为了应用每次重试的超时,在 Polly 的 TimeoutPolicy 之前配置 RetryPolicy

在此案例中,你可能希望如果任何重试超时后重试策略进行重试。为达到此目的,让重试策略处理 Polly 超时策略抛出的 TimeoutRejectedException 异常。

下面的示例使用前面说明的 Polly.Extensions.Http 包,来使用额外的处理扩展方便的错误集 (HttpRequestException, HTTP 5XX, and HTTP 408) with extra handling:

using Polly.Extensions.Http;

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .Or<TimeoutRejectedException>() // thrown by Polly's TimeoutPolicy if the inner call times out
    .WaitAndRetryAsync(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        });

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(10); // Timeout for an individual try

serviceCollection.AddHttpClient("GitHub", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    client.Timeout = TimeSpan.FromSeconds(60); // Overall timeout across all tries
})
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler(timeoutPolicy); // We place the timeoutPolicy inside the retryPolicy, to make it time out each try.

总是对 HttpClient 使用 TimeoutPolicy 优化。(这是默认,这是默认的,所以不需要显式,代码示例如上)

如果在 RetryPolicy 之外配置 TimeoutPolicy

If you have configured the retry and timeout policy in the other order (configuring timeoutPolicy before, thus outside, the retryPolicy), that TimeoutPolicy will instead act as an overall timeout for the whole operation (just as HttpClient.Timeout does), not as a timeout-per-try. This is a natural consequence of the way multiple policies act as nested steps in a middleware pipeline.

Use Case: scoping CircuitBreakers and Bulkheads

Policy instances applied to a named HttpClient configuration are shared across all calls through that HttpClient configuration.

For the stateful policy circuit breaker, this means that all calls through a named HttpClient configured with a circuit-breaker will share that same circuit state.

This usually plays well with HttpClients configured via HttpClient factory, because those HttpClients typically define a common BaseAddress, meaning all calls are to some endpoint on that same BaseAddress. In that case, we might expect that if one endpoint on BaseAddress is unavailable, others will be too. The scoping then plays well: if calls to one endpoint through that HttpClient configuration break the circuit, the circuit will also be broken for others.

If, however, this 'shared' scoping of the circuit-breaker is not appropriate for your scenario, define separate named HttpClient instances and configure each with a separate circuit-breaker policy instance.

The same consideration applies if you use Polly's other stateful policy, Bulkhead. With a Bulkhead policy applied to a named HttpClient configuration, the Bulkhead capacity will be shared across all calls placed through that HttpClient.

Use Case: CachePolicy

Polly CachePolicy can be used in a DelegatingHandler configured via IHttpClientFactory. Polly is generic (not tied to Http requests), so at time of writing, the Polly CachePolicy determines the cache key to use from the Polly.Context. This can be set on an HttpRequestMessage request immediately prior to placing the call through HttpClient, by using an extension method: (add using Polly; to access the extension method)

request.SetPolicyExecutionContext(new Polly.Context("CacheKeyToUseWithThisRequest"));
Using CachePolicy with HttpClientFactory thus also requires that you use overloads on HttpClient which take an HttpRequestMessage as an input parameter.

Some additional considerations flow from the fact that caching with Polly CachePolicy in a DelegatingHandler caches at the HttpResponseMessage level.

Is caching at the HttpResponseMessage level the right fit?

If the HttpResponseMessage is the end content you wish to re-use (perhaps to re-serve in whole or in part), then caching at the HttpResponseMessage level may be a good fit.

In cases such as calling to a webservice to obtain some serialized data which will then be deserialized to some local types in your app, HttpResponseMessage may not be the optimal granularity for caching.

In these cases, caching at the HttpResponseMessage level implies subsequent cache hits repeat the stream-read and deserialize-content operations, which is unnecessary from a performance perspective.

It may be more appropriate to cache at a level higher-up - for example, cache the results of stream-reading and deserializing to the local types in your app.

Considerations when caching HttpResponseMessage
  • The HttpResponseMessage can contain HttpContent which behaves like a forward-only stream - you can only read it once. This can mean that when CachePolicy retrieves it from cache the second time, the stream cannot be re-read unless you also reinitialise the stream pointer.

  • Consider de-personalisation and timestamping. Personal information (if any) and timestamps from a cached result may not be appropriate to re-supply to later requesters.

  • Exercise care to only cache 200 OK responses. Consider using code such as response.EnsureSuccessStatusCode(); to ensure that only successful responses pass to the cache policy. Or you can use a custom ITtlStrategy as described here.

Use Case: Exchanging information between policy execution and calling code

An execution-scoped instance of the class Polly.Context travels with every execution through a Polly policy. The role of this class is to provide context and to allow the exchange of information between the pre-execution, mid-execution, and post-execution phases.

For executions through HttpClients configured with Polly via HttpClientFactory, you can use the extension method HttpRequestMessage.SetPolicyExecutionContext(context), prior to execution, to set the Polly.Context that will be used with the Http call. Context has dictionary-semantics, allowing you to pass any arbitrary data.

var context = new Polly.Context();
context["MyCustomData"] = foo;

HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.SetPolicyExecutionContext(context); 

var response = await client.SendAsync(request, cancellationToken);
// (where client is an HttpClient instance obtained from HttpClientFactory)

Polly passes that Context instance as an input parameter to any delegate hooks such as onRetry configured on the policy. For example, the HttpClient may have been pre-configured with a policy:

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    },
    onRetryAsync: async (outcome, timespan, retryCount, context) => {
        /* Do something with context["MyCustomData"] */
        // ...
    });

Delegate hooks may also set information on Context:

var retryPolicy = HttpPolicyExtensions
    .HandleTransientHttpError()
    .WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(5),
        TimeSpan.FromSeconds(10)
    },
    onRetryAsync: async (outcome, timespan, retryCount, context) => {
        context["RetriesInvoked"] = retryCount;
        // ...
    });

services.AddHttpClient("MyResiliencePolicy", /* etc */)
    .AddPolicyHandler(retryPolicy);

And this information can be read from the context after execution:

var response = await client.SendAsync(request, cancellationToken);

var context = response.RequestMessage?.GetPolicyExecutionContext(); // (if not already held in a local variable)
if (context?.TryGetValue("RetriesInvoked", out int? retriesNeeded) ?? false)
{
    // Do something with int? retriesNeeded
}

Note that the context from HttpRequestMessage.GetPolicyExecutionContext() is only available post-execution if you used HttpRequestMessage.SetPolicyExecutionContext(Context) to set a context prior to execution.

Configuring policies to use services registered with DI, such as ILogger<T>

You may want to configure a policy which makes use of other services registered for Dependency Injection. A typical example would be to configure a policy whose callback delegates require an ILogger resolved by dependency-injection.

An .AddPolicyHandler(...) overload exists allowing you to configure a policy which can resolve services from IServiceProvider when the policy is created.

Because the typical .NET Core logging pattern prefers generic ILogger , this approach plays well with typed clients.

services.AddHttpClient<MyServiceHttpClient>(/* etc */)
    .AddPolicyHandler((services, request) => HttpPolicyExtensions.HandleTransientHttpError()
        .WaitAndRetryAsync(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        },             
        onRetry: (outcome, timespan, retryAttempt, context) =>
        {
            services.GetService<ILogger<MyServiceHttpClient>>()?
                .LogWarning("Delaying for {delay}ms, then making retry {retry}.", timespan.TotalMilliseconds, retryAttempt);
        }
        ));

Note that the policy here obtains ILogger<MyServiceHttpClient>:

services.GetService<ILogger<MyServiceHttpClient>>() /* etc */

which means the logging will be categorised with MyServiceHttpClient. If you want the logging to be categorised with the class consuming the policy, and if multiple classes might consume the policy (meaning you do not know T for ILogger at configuration time), then you can instead use the approach in the section below: pass an ILogger to the policy at runtime using Polly.Context.

If you experience problems with logging being visible with IHttpClientFactory and Azure Functions, check out this discussion and this sample (September 2019; issue may resolve once azure-functions-host/issues/4345 closes).

Configuring HttpClientFactory policies to use an ILogger<T> from the call site

The technique in the section above resolves an ILogger from IServiceProvider at execution time, but the actual category SomeConcreteType of ILogger is defined at configuration time, and the technique relies on dynamic creation of policies.

There are two use cases which that approach does not suit:

  • You want to store policies in PolicyRegistry (as shown earlier in this documentation). These policies are typically created once only during StartUp, not dynamically, so do not fit the dynamic technique of creating a new policy which resolves a new ILogger at execution time.

  • You want the category T of the logger ILogger to be resolved at the call site, not at configuration time. For example, if the class consuming the HttpClient configured with the policy is MyFooApi, then MyFooApi might receive an ILogger in its constructor by dependency injection, and you might want to use that ILogger for the logging done by the policy's onRetry delegate.

Both cases can be solved by passing the ILogger to the policy at the point of execution, using the execution-scoped Polly.Context. The general approach of using Context to pass information to a policy at execution time is described in an earlier section of this documentation.

To pass an ILogger to a Polly policy via Polly.Context, we will first define some helper methods on Polly.Context:

public static class PollyContextExtensions
{
    private static readonly string LoggerKey = "ILogger";

    public static Context WithLogger<T>(this Context context, ILogger logger)
    {
        context[LoggerKey] = logger;
        return context;
    }
    
    public static ILogger GetLogger(this Context context)
    {
        if (context.TryGetValue(LoggerKey, out object logger))
        {
            return logger as ILogger;
        }
    
        return null;
    }

Note that these methods use the base interface Microsoft.Extensions.Logging.Logger because the policy's onRetry or onRetryAsync delegate is not generic and will not know the generic type T. But the actual instance passed at runtime will still be an ILogger .

Configuring the policy in your StartUp class might then look something like this:

var registry = new PolicyRegistry()
{
    { 
        "MyRetryPolicyResolvingILoggerAtRuntime", 
        HttpPolicyExtensions
            .HandleTransientHttpError()
            .WaitAndRetryAsync(new[]
        {
            TimeSpan.FromSeconds(1),
            TimeSpan.FromSeconds(5),
            TimeSpan.FromSeconds(10)
        },             
        onRetry: (outcome, timespan, retryAttempt, context) =>
        {
            context.GetLogger()?.LogWarning("Delaying for {delay}ms, then making retry {retry}.", timespan.TotalMilliseconds, retryAttempt);
        })
    },
};

services.AddPolicyRegistry(registry);

services.AddHttpClient("MyFooApiClient", /* etc */)
    .AddPolicyHandlerFromRegistry("MyRetryPolicyResolvingILoggerAtRuntime");

The example above has stored the policies in a PolicyRegistry and asked HttpClientFactory to retrieve them from the policy registry, but you can also use this technique with the HttpClientFactory overloads which do not involve a policy registry.

Finally, at the call site, where you execute through the HttpClient, you set the ILogger on the Polly.Context before executing. An example class consuming the above policy might look something like this:

public class MyFooApi 
{
    private readonly IHttpClientFactory httpClientFactory;
    private readonly ILogger<MyFooApi> logger;

    public MyFooApi(IHttpClientFactory httpClientFactory, ILogger<MyFooApi> logger)
    {
        this.logger = logger;
        this.httpClientFactory = httpClientFactory;
    
        // If MyFooApi is configured with Transient or Scoped lifetime, 
        // you could alternatively call
        //     client = _httpClientFactory.CreateClient("MyFooApiClient") 
        // here, and store private readonly HttpClient client, rather than IHttpClientFactory
    }
    
    public Task<SomeReturnType> SomeAction(...)
    {
        // (definition of SomeRequestUri and SomeCancellationToken omitted for brevity)
    
        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, SomeRequestUri);
    
        var context = new Polly.Context().WithLogger(logger);
        request.SetPolicyExecutionContext(context); 
    
        var client = httpClientFactory.CreateClient("MyFooApiClient"); 
    
        var response = await client.SendAsync(request, SomeCancellationToken);
    
        // check for success, process the response and return it as SomeReturnType
    }

}

With this technique, you have to use one of the HttpClient.SendAsync(...) overloads taking an HttpRequestMessage parameter, as shown above.

The above examples use a named HttpClient configuration, but the same pattern can also be used with typed-clients on HttpClientFactory.

原文地址:https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值