Reputation: 583
I have got an web application that lacks to free memory.
I suspect HttpClient to be one of the issues, because the object count of HttpClient is increasing over time.
Therefore I want to migrate to the managed IHttpClientFactory, but now I'm stuck with how to best implement the call to the token service (I thought about using the typed client variant).
Right now it's implemented this way:
var myClient = new MyClient(credentials, baseUri, tokenUri, timeout);
Inside of MyClient HttpClient(1) takes care of calling the token service (credentials, tokenUri), storing the expiry date and returning the bearer token to HttpClient(2) that calls the endpoint (baseUri, timeout).
If myClient now tries to fetch some data, it checks if the token needs to be refreshed, if not it fetches the data.
How would I do this with IHttpClientFactory?
Do I still need to handle HttpClient(1) myself (expiry date) or will the factory somehow detect if it needs to refresh the token or not?
I at least understood, that the factory decides if a connection stays open or not.
Upvotes: 1
Views: 355
Reputation: 3857
It sounds like you're on the right track with the transition to HttpClientFactory, and particularly a typed HttpClient.
Under the hood, HttpClientFactory's default implementation manages the pooling and disposal of the underlying primary message handler, which means that the actual HttpClient sitting on top of it can start being generated and disposed in a scoped fashion rather than trying to manage some global, long-running instance of it or creating and tearing down one-off instances, which is well described in Microsoft's own documentation: Use IHttpClientFactory to implement resilient HTTP requests
In cases like yours where the HttpClient was potentially long-lived, it may have made sense for the client itself to manage state within its instance (such as the token), but you end up needing to take a different path now that the client can (and should) be disposed of more frequently.
Do I still need to handle HttpClient(1) myself (expiry date) or will the factory somehow detect if it needs to refresh the token or not?
Yes you still need to handle it, but the HttpClientFactory pattern gives you some tools to help manage it. Since you're inherently leaning into dependency injection with the use of HttpClientFactory, there's a couple different paths you might go.
At the most basic would be just to add some sort of singleton token provider that manages the tokens for you and can be injected into the typed client by the DI container:
public interface ITokenProvider
{
string GetToken(string key);
void StoreToken(string key, string token);
}
// Incredibly basic example, not thread safe, etc...
public class InMemoryTokenProvider : ITokenProvider
{
private readonly Dictionary<string, string> _tokenList = new Dictionary<string, string>();
public string GetToken(string key)
{
return _tokenList.GetValueOrDefault(key);
}
public void StoreToken(string key, string token)
{
_tokenList.Remove(key); // upsert, you get the point...
_tokenList.Add(key, token);
}
}
public class TypedClient
{
private readonly HttpClient _client;
private readonly ITokenProvider _tokenProvider;
public TypedClient(HttpClient client, ITokenProvider tokenProvider)
{
_client = client;
_tokenProvider = tokenProvider;
}
public async Task DoYourThing()
{
var token = _tokenProvider.GetToken("token_A");
// ... if it failed, then UpdateTheAuth()
}
private async Task UpdateTheAuth()
{
var result = await _client.GetAsync("the auth process");
string token = "whatever";
// ...
_tokenProvider.StoreToken("token_A", token);
}
}
When you do your service registration at the start and register the token provider as a singleton, all your state (such as the token) is no longer part of the client itself, so your client can now be disposed and injected wherever. That provider could also be written off to a cache or a database, too.
This can still be a little clunky because its still putting all the logic for calling, failing, updating auth, retrying, etc. within your typed client logic -- it might be good enough if that covers what you need, or you may want something more robust. HttpClientFactory makes it easy to add a delegating handler pipeline as well as policies for resiliency with Polly, such as retry:
services.AddTransient<ExampleDelegatingHandler>();
services.AddHttpClient<IMyHttpClient, MyHttpClient>()
.AddHttpMessageHandler<TokenApplicationHandler>()
.AddPolicyHandler(GetRetryPolicy()); // see Microsoft link
The delegating handler pipeline attaches to your typed client and runs like middleware for every request and response (and can modify them in flight), so you could even move some of this token management off into a delegating handler instead:
public class TokenApplicationHandler : DelegatingHandler
{
private readonly ITokenProvider _tokenProvider;
private readonly IAuthRenewerClient _authRenewer;
public TokenApplicationHandler(ITokenProvider tokenProvider, IAuthRenewerClient authRenewer)
{
_tokenProvider = tokenProvider;
_authRenewer = authRenewer;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// All just demo level, take the implementation with a grain of salt...
string token = _tokenProvider.GetToken("token_A");
request.Headers.Add("x-token-header", token);
var response = await base.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode && response.StatusCode == HttpStatusCode.Unauthorized)
{
string newToken = _authRenewer.RefreshAuth();
_tokenProvider.StoreToken("token_A", newToken);
}
return response;
}
}
Paired with a retry policy, now any time a request goes out and comes back with an Unauthorized
response, your delegating handler can handle the renewal and then the request gets resent the new token, and your typed HttpClient doesn't need to be any the wiser (or even necessarily deal with auth at all).
Key takeaways, make sure as you transition to this pattern that you're disposing of the clients you're creating when you're done with whatever scope they're in so HttpClientFactory can do its background magic.
Upvotes: 2