TL;DR: You need to define a communication protocol between a RetryPolicy
, a DelegatingHandler
and a TokenService
.
In case of Typed Clients you can explicitly call the ExecuteAsync
and use the Context
to exchange data between the to-be-decorated method and the onRetry(Async)
delegate.
This trick can’t be used in a named client situation. What you need to do instead:
- Separate out the Token management into a dedicated service
- Use a
DelegatingHandler
to intercept theHttpClient
‘s communication
This sequence diagram depicts the communication between the different components
Token Service
The DTO
public class Token
{
public string Scheme { get; set; }
public string AccessToken { get; set; }
}
The interface
public interface ITokenService
{
Token GetToken();
Task RefreshToken();
}
The dummy implementation
public class TokenService : ITokenService
{
private DateTime lastRefreshed = DateTime.UtcNow;
public Token GetToken()
=> new Token { Scheme = "Bearer", AccessToken = lastRefreshed.ToString("HH:mm:ss")};
public Task RefreshToken()
{
lastRefreshed = DateTime.UtcNow;
return Task.CompletedTask;
}
}
The registration into the DI as Singleton
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ITokenService, TokenService>();
...
}
Delegating Handler
The custom exception
public class OutdatedTokenException : Exception
{
}
The handler (interceptor)
public class TokenFreshnessHandler : DelegatingHandler
{
private readonly ITokenService tokenService;
public TokenFreshnessHandler(ITokenService service)
{
tokenService = service;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = tokenService.GetToken();
request.Headers.Authorization = new AuthenticationHeaderValue(token.Scheme, token.AccessToken);
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new OutdatedTokenException();
}
return response;
}
}
- It retrieves the current token from the
TokenService
- It sets the authorization header
- It executes the base method
- It checks the response’s status
- If 401 then it throws the custom exception
- If other than 401 then it returns with the response
The registration into the DI as Transient
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ITokenService, TokenService>();
services.AddTransient<TokenFreshnessHandler>();
...
}
Retry Policy
The policy definition
public IAsyncPolicy<HttpResponseMessage> GetTokenRefresher(IServiceProvider provider)
{
return Policy<HttpResponseMessage>
.Handle<OutdatedTokenException>()
.RetryAsync(async (_, __) => await provider.GetRequiredService<ITokenService>().RefreshToken());
}
- It receives an
IServiceProvider
to be able to access theTokenService
- It performs a single retry if an
OutdatedTokenException
was thrown - Inside the
onRetryAsync
delegate it calls theTokenService
‘sRefreshToken
method
Putting all things together
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ITokenService, TokenService>();
services.AddTransient<TokenFreshnessHandler>();
services.AddHttpClient("TestClient")
.AddPolicyHandler((provider, _) => GetTokenRefresher(provider))
.AddHttpMessageHandler<TokenFreshnessHandler>();
...
}
- Please bear in mind that the ordering of
AddPolicyHandler
andAddHttpMessageHandler
matters - If you would call the
AddHttpMessageHandler
first and then theAddPolicyHandler
in that case your retry would not be triggered