I'm implementing resilience for my .NET Core Web Api, and I need a way to pass a value using the ResilienceContext
, but when I'm debugging, the args.Context.Properties
is always empty. I don't know what I'm missing.
I'm using Microsoft.Extensions.Http.Resilience
that uses Polly
Here's the code the I have:
public static class ResilienceKeys
{
public static readonly ResiliencePropertyKey<string> LocationId = new("location-id");
}
// WordPressService.cs
public class WordPressService : IWordPressService
{
private readonly HttpClient _httpClient;
private readonly ResiliencePipelineProvider<string> _pipelineProvider;
public WordPressService(HttpClient httpClient, ResiliencePipelineProvider<string> pipelineProvider)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.example.com/");
_pipelineProvider = pipelineProvider;
}
public async Task<string> GetBannersByLocationAsync(int locationId, CancellationToken cancellationToken)
{
var pipeline = _pipelineProvider.GetPipeline<HttpResponseMessage>("my-pipeline");
var context = ResilienceContextPool.Shared.Get(cancellationToken);
context.Properties.Set(ResilienceKeys.LocationId, locationId.ToString());
var response = await pipeline.ExecuteAsync<HttpResponseMessage, ResilienceContext>(
async (context, cancellationToken) =>
{
// Simulate an error for testing purposes
await Task.Delay(100, cancellationToken);
var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
return response;
},
context,
cancellationToken);
ResilienceContextPool.Shared.Return(context);
return await response.Content.ReadAsStringAsync(cancellationToken);
}
}
Also I'm using dependency injection to add the ResiliencePipeline, with a RetryStrategy
and a FallbackStrategy
to get content from a database in case of the WordPress server is not responding
// Program.cs
builder.Services.AddResiliencePipeline<string, HttpResponseMessage>("my-pipeline", (pipelineBuilder, context) =>
{
var predicateBuilder = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>()
.HandleResult(r => r.StatusCode >= HttpStatusCode.InternalServerError);
pipelineBuilder
.AddFallback(new FallbackStrategyOptions<HttpResponseMessage>()
{
ShouldHandle = predicateBuilder,
FallbackAction = async (args) =>
{
if (args.Context.Properties.TryGetValue(ResilienceKeys.LocationId, out var data))
{
Console.WriteLine("FallbackAction, Location Id: {0}", data);
}
var fallbackResponse = new HttpResponseMessage()
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("Fallback content")
};
return Outcome.FromResult(fallbackResponse);
}
})
.AddRetry(new RetryStrategyOptions<HttpResponseMessage>()
{
ShouldHandle = predicateBuilder,
Delay = TimeSpan.FromSeconds(2),
MaxRetryAttempts = 2,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
});
});
The short answer is that you have used a wrong overload of ExecuteAsync
. You should use this one:
var response = await pipeline.ExecuteAsync<HttpResponseMessage>(
async ctx =>
{
// Simulate an error for testing purposes
await Task.Delay(100, ctx.CancellationToken);
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
},
context);
The overload, what you have used, is not taking advantage of the Context
. If you see the method signature then it becomes clear.
public async ValueTask<TResult> ExecuteAsync<TResult, TState>(
Func<TState, CancellationToken, ValueTask<TResult>> callback,
TState state,
CancellationToken cancellationToken = default)
You have passed the Context
as the state
object. Here we have documented what is the difference between Context
and State
.
Currently there is no such overload which anticipates both a CancellationToken
and a Context
. But you don't even need one.
Whenever you request a Context
from the ResilienceContextPool
then the passed CancellationToken
is attached to the retrieved Context
. If you take a look at the suggested solution's code fragment then you can spot that the CancellationToken
was accessed via the Context
(ctx.CancellationToken
).
So, you should use the following overload:
public async ValueTask<TResult> ExecuteAsync<TResult>(
Func<ResilienceContext, ValueTask<TResult>> callback,
ResilienceContext context)
As a side note please don't forget to return back the borrowed resilience context to the pool:
ResilienceContextPool.Shared.Return(context);