Named Http Clients created by an Http Client Factory is an efficient way to manage calls to 3rd party services such Google’s Captcha and other APIs your app as a unit is a client of.
First, let’s take a look at how we might handle Google Captcha
We register a new Http Client named captcha
, setting a timeout of 5 seconds, and a retry policy of 5 times with a 300ms backoff period, and recording metrics with prometheus, served by our /metrics endpoint
services.AddHttpClient("captcha", config => { config.BaseAddress = new Uri("https://www.google.com/"); config.Timeout = new TimeSpan(0, 0, 5); }) .ConfigurePrimaryHttpMessageHandler(() => { return new SocketsHttpHandler { UseCookies = false }; }) .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(5, retryAttempt => TimeSpan.FromMilliseconds(300 * retryAttempt))) .UseHttpClientMetrics();
Now that we have it registered, when we need to use it, we just inject a IHttpClientFactory
and get the client from a pool
using (var client = _clientFactory.CreateClient("captcha")) { var response = await client.PostAsync("recaptcha/api/siteverify", new FormUrlEncodedContent(new Dictionary<string, string?> { { "secret", _captchaSettings.Value.Secret }, { "response", model.Token }, { "remoteip", Request.HttpContext.Connection.RemoteIpAddress?.ToString() } }), ct); response.EnsureSuccessStatusCode(); var captchaResponse = await response.Content.ReadFromJsonAsync<CaptchaResponse>(cancellationToken: ct); if (!captchaResponse!.Success) { ModelState.AddModelError(nameof(model.Token), "Invalid captcha!"); _logger.LogWarning("Invalid captcha sending message"); return ValidationProblem(ModelState); } ... }
Now let’s consider a case where we need to retrieve an access token that’s valid for an hour, and we want to reuse it, and have it automatically added to any request made to that service. In this case let’s use an identity provider, in an app that would make a lot calls to such a service to manage users, reset passwords, etc.
We inject a MemoryCache
instance to store our token for slightly less than its expiration time, to give us room for our calls to complete before expiration, in this case a wide margin of 2 minutes is used. We’ll also inject our IOptions of Autehntication Settings, and we’ll get our token using OAuth 2.0 / client_credentials, with all the things we added previously.
services.AddHttpClient("idP", (sp, httpClient) => { var cache = sp.GetRequiredService<IMemoryCache>(); var authenticationSettings = sp.GetRequiredService<IOptions<AuthenticationSettings>>(); httpClient.BaseAddress = new authenticationSettings.Value.IdPBaseUrl; var token = cache.Get<string>("IdPToken"); if (token == null) { var tokenResponse = httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = authenticationSettings.Value.Authority + "/connect/token", ClientId = authenticationSettings.Value.IdPClientId, ClientSecret = authenticationSettings.Value.IdPClientSecret, Scope = authenticationSettings.Value.Scope }).ConfigureAwait(false).GetAwaiter().GetResult(); token = tokenResponse.AccessToken; cache.Set("IdPToken", token, new TimeSpan(0, 0, tokenResponse.ExpiresIn - 180)); } httpClient.SetBearerToken(token); }) .ConfigurePrimaryHttpMessageHandler(() => { return new SocketsHttpHandler { UseCookies = false }; }) .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(20, retryAttempt => TimeSpan.FromMilliseconds(300 * retryAttempt))) .UseHttpClientMetrics();
Now, anywhere we need to make calls to our Identity Provider, we can just create an HttpClient
from the IHttpClientFactory
and our authentication is handled for us, efficently.
using (var httpClient = _clientFactory.CreateClient("idP")) { ... }
Quick Links
Legal Stuff