I'm looking for a way to attach a generic Polly fallback policy to my typed HttpClient. My policy gets applied to certain request types as and when required, otherwise it applies a NoOpPolicy. Im interested in the onFallbackAsync Task. I need to save the request/response to a database table, but I'm struggling to figure out how to apply this because I can't see how to inject a dependency. I seem to be missing something obvious, but I don't know what. Can this be done with a delegating handler?

My typed client basically looks like this, and is used for a variety of different API calls:

public class ApiClient : IApiClient
{
    private readonly HttpClient _httpClient;
    private readonly ILogger _logger;

    private HttpRequestMessage _httpRequestMessage;
    private HttpContent _httpContent;

    private IDictionary<string, string> _queryParameters;

    public ApiClient(HttpClient httpClient, ILogger logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        _httpRequestMessage = new HttpRequestMessage();
        _queryParameters = new Dictionary<string, string>();
    }

    public void SetUrl(string urlPath)
    {
        urlPath = urlPath.TrimEnd('/');
        _httpClient.BaseAddress = new Uri(urlPath);
    }

    public void SetQueryParamters(IDictionary<string, string> parameters)
    {
        _queryParameters = parameters;
    }

    public void AddQueryParamter(string key, string value)
    {
        _queryParameters.Add(key, value);
    }

    public async Task<T> Get<T>()
    {
        _httpRequestMessage.Method = HttpMethod.Get;

        return await EnvokeService<T>();
    }

    public async Task<T> PostJsonAsync<T>(object body)
    {
        var settings = new JsonSerializerSettings()
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        };

        _httpContent = new JsonContent(JsonConvert.SerializeObject(body, settings));
        _httpRequestMessage.Method = HttpMethod.Post;

        return await EnvokeService<T>();
    }

    private async Task<T> EnvokeService<T>()
    {
        string responseContent = null;
        try
        {
            _httpRequestMessage.Content = _httpContent;
            _httpClient.DefaultRequestHeaders.Add("Accept", "application/json");

            var responseMessage = await _httpClient.SendAsync(_httpRequestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
            responseContent = await responseMessage.Content.ReadAsStringAsync();

            if (responseMessage.IsSuccessStatusCode)
            {
                T result;
                var serializer = new JsonSerializer();                    
                using (var sr = new StreamReader(await responseMessage.Content.ReadAsStreamAsync()))
                using (var jsonTextReader = new JsonTextReader(sr))
                {
                    result = serializer.Deserialize<T>(jsonTextReader);
                }
                _logger.Debug(logMessage);
                return result;
            }

            throw new HttpRequestException($"Error Code: {responseMessage.StatusCode}. Response: {responseContent}");
        }
        catch (Exception e)
        {
            _logger.Exception(e, "Error");
            throw;
        }
    }
}

It is setup like this:

public static class ApiHttpClientConfigurator
{
    public static void AddApiHttpClient (this IServiceCollection services)
    {
        IPolicyRegistry<string> registry = services.AddPolicyRegistry();

        registry.Add("WaitAndRetryPolicy", ResiliencePolicies.GetHttpPolicy(new TimeSpan(0, 1, 0)));
        registry.Add("NoOpPolicy", Policy.NoOpAsync().AsAsyncPolicy<HttpResponseMessage>());

        services.AddHttpClient<ApiHttpClient>().AddPolicyHandlerFromRegistry(PolicySelector);
        services.AddSingleton<IApiHttpClientFactory, ApiHttpClientFactory>();
    }

    private static IAsyncPolicy<HttpResponseMessage> PolicySelector(IReadOnlyPolicyRegistry<string> policyRegistry, HttpRequestMessage httpRequestMessage)
    {
        // if we have a message of type X then apply the policy
        if (httpRequestMessage.RequestUri.AbsoluteUri.Contains("/definitely/apply/a/retry/policy"))
        {
            return policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>("WaitAndRetryPolicy");
        }

        return policyRegistry.Get<IAsyncPolicy<HttpResponseMessage>>("NoOpPolicy");
    }
}

Registered as so:

services.AddApiHttpClient();

My policies are defined as such:

public static class ResiliencePolicies
{
    public static IAsyncPolicy<HttpResponseMessage> HttpFallBackPolicy
    {
        get => Policy<HttpResponseMessage>.Handle<Exception>().FallbackAsync(new HttpResponseMessage(HttpStatusCode.InternalServerError), d =>
        {
            // TODO: publish event
            InstanceOfMediatr.Publish(new RequestFailedEvent(request/response/data/here))
            Log.Warning($"Fallback: {d.Exception.GetType().Name} {d.Exception.Message}");
            return Task.CompletedTask;
        });
    }

    public static IAsyncPolicy<HttpResponseMessage> HttpRetryPolicy
    {
        get => HttpPolicyExtensions.HandleTransientHttpError()
            .Or<TimeoutRejectedException>()
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt * 0.5));
    }

    public static IAsyncPolicy<HttpResponseMessage> GetHttpTimeoutPolicy(TimeSpan timeSpan) =>
        Policy.TimeoutAsync<HttpResponseMessage>(timeSpan);

    public static IAsyncPolicy<HttpResponseMessage> GetHttpPolicy(TimeSpan timeout) =>
        Policy.WrapAsync(HttpFallBackPolicy, HttpRetryPolicy, GetHttpTimeoutPolicy(timeout));
}

The final part of the puzzle would appear to be, how can I complete the TODO section above? I seem to need a dynamic fallback action, but I'm really not sure how to implement it, or whether or not it is even possible.

1

There are 1 best solutions below

0
On

Just in case anyone else comes across this problem, there are two ways to solve it depending upon your requirements (using AddPolicyHandlerFromRegistry() or AddPolicyHandler()).

Through IServiceProvider

AddPolicyHandler() has a convenient overload where you can inject IServiceProvider:

.AddPolicyHandler((provider, message) => 
    HttpPolicyExtensions
        .HandleTransientHttpError()
        .FallbackAsync(
            fallbackValue: new HttpResponseMessage(HttpStatusCode.InternalServerError),
            onFallbackAsync: (result, context) =>
            {
                var publisher = provider.GetService<IPublisher>();
                publisher.Publish(new HttpRequestFallbackEvent());

                return Task.CompletedTask;
            }))

Through Context

If you are using a PolicyRegistry and AddPolicyHandlerFromRegistry(), then it is easier to use the Context as described here. First extend the Polly's Context:

public static class ContextExtensions
{
    private static readonly string PublisherKey = "PublisherKey";

    public static Context WithDependencies(this Context context, IPublisher publisher)
    {
        context[PublisherKey] = publisher;
        return context;
    }
    
    public static IPublisher GetPublisher(this Context context)
    {
        if (context.TryGetValue(PublisherKey, out object publisher))
        {
            return publisher as IPublisher;
        }
        return null;
    }
}

Then in your client, inject your dependency (i.e. IPublisher) and add it to the new Context and add that to the executing context:

var context = new Context().WithDependencies(_publisher);
request.SetPolicyExecutionContext(context);

var responseMessage = await _httpClient.SendAsync(request)

Now you can use that in your registered and selected policy:

private static IAsyncPolicy<HttpResponseMessage> FallbackPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .Or<Exception>()
        .FallbackAsync(
            fallbackValue: new HttpResponseMessage(HttpStatusCode.InternalServerError),
            onFallbackAsync: (result, context) =>
                {
                    var publisher = context.GetPublisher();
                    publisher.Publish(new HttpRequestFallbackEvent());
    
                    return Task.CompletedTask;
                }));
}