James Hancock
James Hancock

Reputation: 3547

How do you override AddStandardResilenceHandler() for a specific client?

Given the following is set as the default for all clients in the C# project:

builder.Services.ConfigureHttpClientDefaults(http => {
    // Turn on resilience by default
    http.AddStandardResilienceHandler();

});

How does one override the values of that StandardResilienceHandler for a specific Client?

I've tried the following variations:

    services.AddHttpClient<IMyClient, MyClient>()           
        .AddResilienceHandler("MyClient", (context, next) => {
            context.AddTimeout(TimeSpan.FromMinutes(10));
        });

ignored

.AddStandardResilienceHandler(options => {
                options.AttemptTimeout = new HttpTimeoutStrategyOptions {
                    Timeout = TimeSpan.FromMinutes(5)
                };
                options.TotalRequestTimeout = new HttpTimeoutStrategyOptions {
                    Timeout = TimeSpan.FromMinutes(15)
                };
                options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(10);
            });

Also ignored.

I also created a policy like this:

var retryPolicy = HttpPolicyExtensions

                    .HandleTransientHttpError()
                    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))
                    .WrapAsync(Policy.TimeoutAsync<HttpResponseMessage>(
                        TimeSpan.FromMinutes(5),
                        TimeoutStrategy.Optimistic));

and then used this:

.AddPolicyHandler(retryPolicy)

It still uses the default no matter what.

You'd think this would be easy and documented but I can't find anything in search.

Upvotes: 7

Views: 3639

Answers (3)

hansmbakker
hansmbakker

Reputation: 1234

There are multiple things to this:

  1. overriding the settings of AddStandardResilienceHandler itself
  2. overriding the AddStandardResilienceHandler for a specific HttpClient

The answer by @Peter Csala addresses the first part - basically using

http.AddStandardResilienceHandler(options => {
  // set options here
})

The question mentions ConfigureHttpClientDefaults which means that the http.AddStandardResilienceHandler(); applies to all HttpClients.

If you want to replace it for a specific HttpClient, then you could use the workaround by @NatMarchand until something more permanent is done about it in https://github.com/dotnet/extensions/issues/5695.

Bonus: If you want to override settings for a specific endpoint or in general do it based on request message properties, you can do something like the code below.

⚠️Warning: only apply this code on a specific HttpClient because otherwise all outgoing requests are checked, which might cause a performance issue. You can do this e.g. using the workaround by @NatMarchand for this.

builder.AddStandardResilienceHandler(options =>
{
    // This code changes the default AttemptTimeout and TotalRequestTimeout for an endpoint that has a long-running import operation.

    options.AttemptTimeout.TimeoutGenerator = timeoutGenerator(
        options.AttemptTimeout.Timeout,
        TimeSpan.FromSeconds(30));

    options.TotalRequestTimeout.TimeoutGenerator = timeoutGenerator(
        options.TotalRequestTimeout.Timeout,
        options.Retry.MaxRetryAttempts * TimeSpan.FromSeconds(30));

    Func<TimeoutGeneratorArguments, ValueTask<TimeSpan>> timeoutGenerator(TimeSpan defaultTimeout, TimeSpan importTimeout)
    {
        return arguments =>
        {
            // add the using Polly; namespace for GetRequestMessage()
            var tryRequestMessage = arguments.Context.GetRequestMessage();
            var timeout = tryRequestMessage switch
            {
                HttpRequestMessage request when request.RequestUri.AbsolutePath.EndsWith("/import")
                            => ValueTask.FromResult(importTimeout),
                _ => ValueTask.FromResult(defaultTimeout),
            };

            return timeout;
        };
    }
});

Upvotes: 1

Peter Csala
Peter Csala

Reputation: 22829

As far as I know you can't do what you want to achieve.

Whenever you call AddStandardResilienceHandler or AddResilienceHandler it registers a brand new ResilienceHandler. That class is currently treated as internal. (So, calling it multiple times will register multiple ResilienceHandler instances.)

You might have the temptation to retrieve the -standard pipeline itself (that's the name of the pipeline which is registered by the AddStandardResilienceHandler call) through the ResiliencePipelineProvider<string>. The problem with that the retrieved ResiliencePipeline it not adjustable directly. It only supports reload but that won't help you either...

So all in all, currently this use case is not supported. I would recommend to file an issue on the https://github.com/dotnet/extensions/issues repository.


UPDATE #1

Here is an example how to set and retrieve overridden value.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http.Resilience;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

var builder = Host.CreateApplicationBuilder(args);
builder.Services
    .AddHttpClient("test")
    .AddStandardResilienceHandler(options =>
    {
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(11);
    });

var serviceProvider = builder.Services.BuildServiceProvider();
var monitor = serviceProvider.GetRequiredService<IOptionsMonitor<HttpStandardResilienceOptions>>();
var options = monitor.Get("test-standard");
Console.WriteLine(options.AttemptTimeout.Timeout);

using var host = builder.Build();
  • It registers a named HttpClient with test name
  • It adds the standard resilience handler to it
  • It overrides the AttemptTimeout from 10 seconds (the default) to 11
  • It retrieves an IOptionsMonitor from the DI
  • It retrieves that HttpStandardResilienceOptions which is named as test-standard

If you use a typed client (instead of a named client) then the options name will be -standard as mentioned in the original post.

Please note that you can override the options more or less independently. If you want to override AttemptTimeout like in the above example then the rest of the options remain untouched.

I said more or less independently because for example you can't set the AttemptTimeout to 20 seconds because it will throw an OptionsValidationException due to misalignment with Circuit Breaker options:

Microsoft.Extensions.Options.OptionsValidationException: The sampling duration of circuit breaker strategy needs to be at least double of an attempt timeout strategy’s timeout interval, in order to be effective. Sampling Duration: 30s,Attempt Timeout: 20s

Upvotes: 5

Nikolay Konko
Nikolay Konko

Reputation: 1

For those who haven't found an answer. Call Configure() :

.AddStandardResilienceHandler().
Configure(options => ...);

Upvotes: -1

Related Questions