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.
Just in case anyone else comes across this problem, there are two ways to solve it depending upon your requirements (using
AddPolicyHandlerFromRegistry()
orAddPolicyHandler()
).Through
IServiceProvider
AddPolicyHandler()
has a convenient overload where you can injectIServiceProvider
:Through
Context
If you are using a PolicyRegistry and
AddPolicyHandlerFromRegistry()
, then it is easier to use theContext
as described here. First extend the Polly'sContext
:Then in your client, inject your dependency (i.e.
IPublisher
) and add it to the newContext
and add that to the executing context:Now you can use that in your registered and selected policy: