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:
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;
}
[Headers("Authorization: Bearer")]
public interface IBarIntegration
{
[Get("/api/ext/Foo/GetFooBriefInfo")]
Task<ApiResponse<GetFooBriefInfoForFooDto>> GetFooBriefInfo(GetFooBriefInfoForFooInputDto inputData);
}
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;
}
}
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.
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.
Because we can chain the AddRefitClient
, ConfigureHttpClient
and AddPolicyHandler
method calls that's why we can
PolicyRegistry
BarApiClientHelper
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)));
AddRefitClient
receives a Type, which is used to register a typed client
settingsAction
function to setup a RefitSettings
.GetAwaiter().GetResult()
instead of await
CancellationToken.None
to the GetAccessToken
, because here we don't want to cancel this callConfigureHttpClient
is used to set the BaseAddress
for the underlying HttpClient
which will be used by the refit clientAddPolicyHandler
is used to set the resilience policy for the refit clientprivate 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!;
}
IBarApiClientHelper
because the IBarIntegration
can be injected directlyGetFooBriefInfo
method call, because we have already integrated the refit client and the policy during the DI registrationUPDATE #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);
})
...