Home > OS >  Reusing a Polly retrial policy for multiple Refit endpoints without explicitly managing the HttpClie
Reusing a Polly retrial policy for multiple Refit endpoints without explicitly managing the HttpClie

Time:10-16

I am trying to make Refit work with Polly in an ASP.NET Core 6 application. I have a working version, but I feel that there is too much code involved for each new method / consumed API endpoint.

I want to keep things simple for now by defining a retrial policy and using it for multiple endpoints. My code is as follows:

The retrial policy

private static IServiceCollection ConfigureResilience(this IServiceCollection services)
{
    var retryPolicy = Policy<IApiResponse>
        .Handle<ApiException>()
        .OrResult(x => x.StatusCode is >= HttpStatusCode.InternalServerError or HttpStatusCode.RequestTimeout)
        .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), RetryPolicyMaxCount));

    var register = new PolicyRegistry()
    {
        { "DefaultRetrialPolicy", retryPolicy }
    };

    services.AddPolicyRegistry(register);

    return services;
}

The interface to be used with Refit to generate the HTTP calls for the external application:

[Headers("Authorization: Bearer")]
public interface IBarIntegration
{
    [Get("/api/ext/Foo/GetFooBriefInfo")]
    Task<ApiResponse<GetFooBriefInfoForFooDto>> GetFooBriefInfo(GetFooBriefInfoForFooInputDto inputData);
}

Factory class to configure authentication for Refit. Authentication relies OnBehalfOf token generated based on the currently logged-in user access token.

internal sealed class BarApiClientHelper : IBarApiClientHelper
{
    private readonly IOptionsSnapshot<BarApiSettings> _BarSettings;
    private readonly IAccessTokenHelperService _accessTokenHelperService;

    public BarApiClientHelper(IOptionsSnapshot<BarApiSettings> BarSettings, IAccessTokenHelperService accessTokenHelperService)
    {
        _BarSettings = BarSettings;
        _accessTokenHelperService = accessTokenHelperService;
    }

    public async Task<TApiClient> CreateApiClient<TApiClient>(CancellationToken token)
    {
        string baseUrl = _BarSettings.Value.BaseUrl;
        string accessToken = await _accessTokenHelperService.GetAccessToken(token);

        var refitClient = RestService.For<TApiClient>(baseUrl, new RefitSettings
        {
            AuthorizationHeaderValueGetter = () => Task.FromResult(accessToken)
        });

        return refitClient;
    }
}

Example (infrastructure) method that will be called by the business layer.

public async Task<GetFooBriefInfoForFooDto> GetFooBriefInfo(string upn, CancellationToken token)
{
    var apiClient = await _clientHelper.CreateApiClient<IBarIntegration>(token);
    var retrialPolicy = _registry.Get<AsyncRetryPolicy<IApiResponse>>(DefaultRetrialPolicy);

    var func = async () => (IApiResponse) await apiClient.GetFooBriefInfo(new GetFooBriefInfoForFooInputDto { FooContactUpn = upn });
    var FooInfo = (ApiResponse<GetFooBriefInfoForFooDto>) await retrialPolicy.ExecuteAsync(func);
    await FooInfo.EnsureSuccessStatusCodeAsync();

    return FooInfo.Content!;
}

This approach seems to work fine, but I am unhappy with the amount of code required in each business-specific method (GetFooBriefInfo function). Is there any way to simplify this, I feel that I am kind of violating DRY by having each method get the retrial policy, executing and ensuring the success code.


Starting from Peter's great answer the infrastructure code looks as follows:

private static IServiceCollection ConfigureResilience(this IServiceCollection services)
{
    services
        .AddRefitClient(typeof(IBarIntegration), (sp) =>
        {
            var accessTokenHelperService = sp.GetRequiredService<IAccessTokenHelperService>();
            return new RefitSettings
            {
                AuthorizationHeaderValueGetter = () => accessTokenHelperService.GetAccessToken(default)
            };
        })
        .ConfigureHttpClient((sp, client) =>
        {
            var BarSettings = sp.GetRequiredService<IOptions<BarApiSettings>>();
            string baseUrl = BarSettings.Value.BaseUrl;
            client.BaseAddress = new Uri(baseUrl);
        })
        .AddPolicyHandler(Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(x => x.StatusCode is >= HttpStatusCode.InternalServerError or HttpStatusCode.RequestTimeout)
            .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), RetryPolicyMaxCount)));

    return services;
}

I have made some changes and I will clarify them below (I am not sure how good are some of them):

  1. AuthorizationHeaderValueGetter = () => accessTokenHelperService.GetAccessToken(default) is more direct (or perhaps safer than using .Result. However, this means that this will be called for each HTTP call and getting the access token should use some cache. In my case, I am using OBO which adds ~400ms for each call, so using a cache is a must.

In this case, I am not sure if this change is OK from a token acquisition perspective (I guess it is a better choice in the async..await world).

  1. Policy handler has to deal with HttpResponseMessage and HttpRequestException since Polly is applied to the HttpClient.

The actual Refit interface has been simplified to:

[Headers("Authorization: Bearer")]
public interface IBarIntegration
{
    [Get("/api/ext/Foo/GetFooBriefInfo")]
    Task<GetFooBriefInfoForFooDto> GetFooBriefInfo(GetFooBriefInfoForFooInputDto inputData);
}

IBarIntegration can be directly injected now. The only thing I dislike a little is that I had to reference Refit in the "Application" layer (the solution is a "Clean architecture"), but I like simple over yet another abstraction.

CodePudding user response:

Refit defines an extension method against IServiceCollection called AddRefitClient which returns an IHttpClientBuilder. This is good for us, since it is the same interface with which the AddHttpClient extension method returns. So, we can use the AddPolicyHandler, AddTransientHttpErrorPolicy or AddPolicyHandlerFromRegistry methods as well.

Refit client Polly policy

Because we can chain the AddRefitClient, ConfigureHttpClient and AddPolicyHandler method calls that's why we can

  • avoid the usage of the PolicyRegistry
  • avoid the usage of the BarApiClientHelper
  • avoid the explicit usage of the policy itself

Registration

services
.AddRefitClient(typeof(IBarIntegration), (sp) =>
{
    var accessTokenHelperService = sp.GetRequiredService<IAccessTokenHelperService>();
    string accessToken = accessTokenHelperService.GetAccessToken(CancellationToken.None).GetAwaiter().GetResult();
    return new RefitSettings
    {
        AuthorizationHeaderValueGetter = () => Task.FromResult(accessToken)
    };
})
.ConfigureHttpClient((sp, client) =>
{
    var barSettings = sp.GetRequiredService<IOptionsSnapshot<BarApiSettings>>();
    string baseUrl = barSettings.Value.BaseUrl;
    client.BaseAddress = new Uri(baseUrl);
})
.AddPolicyHandler(Policy<IApiResponse>
    .Handle<ApiException>()
    .OrResult(x => x.StatusCode is >= HttpStatusCode.InternalServerError or HttpStatusCode.RequestTimeout)
    .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), RetryPolicyMaxCount)));
  • The AddRefitClient receives a Type, which is used to register a typed client
    • It also anticipates a settingsAction function to setup a RefitSettings
      • Unfortunately this delegate is sync that's why I had to use .GetAwaiter().GetResult() instead of await
      • I've passed a CancellationToken.None to the GetAccessToken, because here we don't want to cancel this call
  • The ConfigureHttpClient is used to set the BaseAddress for the underlying HttpClient which will be used by the refit client
  • The AddPolicyHandler is used to set the resilience policy for the refit client

Usage

private readonly IBarIntegration apiClient; //injected via ctor
public async Task<GetFooBriefInfoForFooDto> GetFooBriefInfo(string upn, CancellationToken token)
{
    var fooInfo = await apiClient.GetFooBriefInfo(new GetFooBriefInfoForFooInputDto { FooContactUpn = upn });
    await fooInfo.EnsureSuccessStatusCodeAsync();
    return fooInfo.Content!;
}
  • There is no need to use the IBarApiClientHelper because the IBarIntegration can be injected directly
  • There is no need to retrieve the Polly policy and manually decorate the GetFooBriefInfo method call, because we have already integrated the refit client and the policy during the DI registration

UPDATE #1
Based on the suggestion of Can Bilgin the registration code could be rewritten like this:

services
.AddRefitClient(typeof(IBarIntegration), (sp) =>
{
    var accessTokenHelperService = sp.GetRequiredService<IAccessTokenHelperService>();
    return new RefitSettings
    {
        AuthorizationHeaderValueGetter = async () => await accessTokenHelperService.GetAccessToken(CancellationToken.None)
    };

})
.ConfigureHttpClient((sp, client) =>
{
    var barSettings = sp.GetRequiredService<IOptionsSnapshot<BarApiSettings>>();
    client.BaseAddress = new Uri(barSettings.Value.BaseUrl);
})
...

CodePudding user response:

Starting from Peter's great answer the infrastructure code looks as follows:

private static IServiceCollection ConfigureResilience(this IServiceCollection services)
{
    services
        .AddRefitClient(typeof(IBarIntegration), (sp) =>
        {
            var accessTokenHelperService = sp.GetRequiredService<IAccessTokenHelperService>();
            return new RefitSettings
            {
                AuthorizationHeaderValueGetter = () => accessTokenHelperService.GetAccessToken(default)
            };
        })
        .ConfigureHttpClient((sp, client) =>
        {
            var BarSettings = sp.GetRequiredService<IOptions<BarApiSettings>>();
            string baseUrl = BarSettings.Value.BaseUrl;
            client.BaseAddress = new Uri(baseUrl);
        })
        .AddPolicyHandler(Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(x => x.StatusCode is >= HttpStatusCode.InternalServerError or HttpStatusCode.RequestTimeout)
            .WaitAndRetryAsync(Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), RetryPolicyMaxCount)));

    return services;
}

I have made some changes and I will clarify them below (I am not sure how good are some of them):

  1. AuthorizationHeaderValueGetter = () => accessTokenHelperService.GetAccessToken(default) is more direct (or perhaps safer than using .Result. However, this means that this will be called for each HTTP call and getting the access token should use some cache. In my case, I am using OBO which adds ~400ms for each call, so using a cache is a must.

In this case, I am not sure if this change is OK from a token acquisition perspective (I guess it is a better choice in the async..await world).

  1. Policy handler has to deal with HttpResponseMessage and HttpRequestException since Polly is applied to the HttpClient.

The actual Refit interface has been simplified to:

[Headers("Authorization: Bearer")]
public interface IBarIntegration
{
    [Get("/api/ext/Foo/GetFooBriefInfo")]
    Task<GetFooBriefInfoForFooDto> GetFooBriefInfo(GetFooBriefInfoForFooInputDto inputData);
}

IBarIntegration can be directly injected now. The only thing I dislike a little is that I had to reference Refit in the "Application" layer (the solution is a "Clean architecture"), but I like simple over yet another abstraction.

  • Related