In many projects we want to call external APIs and use their results in our application. In this article, we will address the following:
HttpClientFactory
Refit
Polly
HttpClientFactory
Microsoft introduced the HttpClient in .Net Framework 4.5 and is the most popular way to consume a Web API in your .NET server-side code. But it has some serious issues like disposing the HttpClient object doesn’t close the socket immediately, too many instances affecting the performance and Singleton HttpClient or shared HttpClient instance not respecting the DNS Time to Live (TTL) settings. HttpClientFactory solves the all these problems. It is one of the newest feature of ASP.NET Core 2.1. It provides a central location for naming and configuring and consuming logical HttpClients in your application, and this post talks about 3 ways to use HTTPClientFactory in ASP.NET Core 2.1.
There are 3 different ways to use it and we’ll see an example of each of them.
Using HttpClientFactory Directly
Named Clients
Typed Clients
Using HttpClientFactory Directly
you’ll always have to register the HttpClient in ConfigureServices method of the Startup.cs class. The following line of code registers HttpClient with no special configuration.
[HttpGet] publicasync Task<ActionResult> Get() { var client = _httpClientFactory.CreateClient(); client.BaseAddress = new Uri("http://api.github.com"); string result = await client.GetStringAsync("/"); return Ok(result); } }
Named Clients
The basic use of HTTPClientFactory in above example is ideal in a situation where you need to make a quick request from a single place in the code. When you need to make multiple requests from multiple places from your code, “Named Clients” will help you. With named clients, you can define the HTTP client with some pre-configured settings which will be applied when creating the HttpClient. Like,
// HERE services.AddHttpClient(); services.AddHttpClient("github", c => { c.BaseAddress = new Uri("https://api.github.com/"); c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); }); }
Here we call AddHttpClient twice, once with the name “github” and once without. The github client has some default configuration applied, namely the base address and two headers required to work with the GitHub API. The overload of AddHttpClient method accepts two parameters, a name and an Action delegate taking a HttpClient which allows us to configure the HttpClient.
You can use named client in the following way in the API controller.
[HttpGet] publicasync Task<ActionResult> Get() { var client = _httpClientFactory.CreateClient("github"); string result = await client.GetStringAsync("/"); return Ok(result); } }
Here, we are passing the registered name of the client in CreateClient() method to create HttpClient. This is useful as the default configuration defined at the time of registration will be pre-applied when we ask for a named client.
Typed Client
Using Typed clients, you can define pre-configuration for your HttpClient inside a custom class. This custom class can be registered as Typed client, and later when needed, it can be injected via the calling class constructor. I prefer Typed Client for the following reasons,
Flexible approach compare to named clients.
You no longer have to deal with strings (like in named clients).
You can encapsulate the HTTP calls and all logic dealing with that endpoint.
Let’s see an example. Below is a custom class defined for Github client.
1 2 3 4 5 6 7 8 9 10 11 12
publicclassGitHubClient { public HttpClient Client { get; privateset; }
This works great. There is another better way of making typed client work. Here, the HttpClient is exposed directly, but you can encapsulate the HttpClient entirely using the following way. First, define a contract for the GitHubClient.
Based on the documentation and results provided as an example, we want to have a strongly typed output so I used json2csharp to convert JSON to C# but I made some changes to the result like the following:
Replace all int with double
Removed Root class
Changed MyArray to Country
JsonProperty uses for Newtonsoft.Json library but if you want to use the new System.Text.Json library, you should change it to JsonPropertyName.
Now, We want to use Refit to fetch data so write the following interface.
1 2 3 4 5 6
publicinterfaceICountryApi { // You have to start the URL with '/' [Get("/{version}/name/{country}")] Task<List<Country>> GetCountry(string version,string country); }
And write your base address in appsettings.json
1 2 3 4 5
// appsettings.json // Don't use '/' at the end of the URL. "MyRefitOptions": { "BaseAddress": "https://restcountries.eu/rest" }
Polly is a .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.
There are many topics in which you can use Polly and for this you should refer to its site. But one of the most important reasons for using Polly is the retry process.
// Retry once Policy .Handle<SomeExceptionType>() .Retry()
// Retry multiple times Policy .Handle<SomeExceptionType>() .Retry(3)
// Retry multiple times, calling an action on each retry // with the current exception and retry count Policy .Handle<SomeExceptionType>() .Retry(3, onRetry: (exception, retryCount) => { // Add logic to be executed before each retry, such as logging });
// Retry multiple times, calling an action on each retry // with the current exception, retry count and context // provided to Execute() Policy .Handle<SomeExceptionType>() .Retry(3, onRetry: (exception, retryCount, context) => { // Add logic to be executed before each retry, such as logging });
// Retry forever, calling an action on each retry with the // current exception Policy .Handle<SomeExceptionType>() .RetryForever(onRetry: exception => { // Add logic to be executed before each retry, such as logging });
// Retry forever, calling an action on each retry with the // current exception and context provided to Execute() Policy .Handle<SomeExceptionType>() .RetryForever(onRetry: (exception, context) => { // Add logic to be executed before each retry, such as logging });
// Retry, waiting a specified duration between each retry. // (The wait is imposed on catching the failure, before making the next try.) Policy .Handle<SomeExceptionType>() .WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) });
// Retry, waiting a specified duration between each retry, // calling an action on each retry with the current exception // and duration Policy .Handle<SomeExceptionType>() .WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) }, (exception, timeSpan) => { // Add logic to be executed before each retry, such as logging });
// Retry, waiting a specified duration between each retry, // calling an action on each retry with the current exception, // duration and context provided to Execute() Policy .Handle<SomeExceptionType>() .WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) }, (exception, timeSpan, context) => { // Add logic to be executed before each retry, such as logging });
// Retry, waiting a specified duration between each retry, // calling an action on each retry with the current exception, // duration, retry count, and context provided to Execute() Policy .Handle<SomeExceptionType>() .WaitAndRetry(new[] { TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(3) }, (exception, timeSpan, retryCount, context) => { // Add logic to be executed before each retry, such as logging });
// Retry a specified number of times, using a function to // calculate the duration to wait between retries based on // the current retry attempt (allows for exponential backoff) // In this case will wait for // 2 ^ 1 = 2 seconds then // 2 ^ 2 = 4 seconds then // 2 ^ 3 = 8 seconds then // 2 ^ 4 = 16 seconds then // 2 ^ 5 = 32 seconds Policy .Handle<SomeExceptionType>() .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) );
// Retry a specified number of times, using a function to // calculate the duration to wait between retries based on // the current retry attempt, calling an action on each retry // with the current exception, duration and context provided // to Execute() Policy .Handle<SomeExceptionType>() .WaitAndRetry( 5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, timeSpan, context) => { // Add logic to be executed before each retry, such as logging } );
// Retry a specified number of times, using a function to // calculate the duration to wait between retries based on // the current retry attempt, calling an action on each retry // with the current exception, duration, retry count, and context // provided to Execute() Policy .Handle<SomeExceptionType>() .WaitAndRetry( 5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, timeSpan, retryCount, context) => { // Add logic to be executed before each retry, such as logging } );
// Wait and retry forever, calling an action on each retry with the // current exception and the time to wait Policy .Handle<SomeExceptionType>() .WaitAndRetryForever( retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, timespan) => { // Add logic to be executed before each retry, such as logging });
// Wait and retry forever, calling an action on each retry with the // current exception, time to wait, and context provided to Execute() Policy .Handle<SomeExceptionType>() .WaitAndRetryForever( retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (exception, timespan, context) => { // Add logic to be executed before each retry, such as logging });
Therefore, according to the above examples, we can implement our scenario as follows to see if the requested country is valid or not.
var result = await retryPolicy.ExecuteAsync(async () => { var status = await _httpClientFactory.CreateClient().GetAsync($"https://restcountries.eu/rest/v2/name/{country}").ConfigureAwait(false); return status.StatusCode == HttpStatusCode.OK; });
return result; } }
Polly & HttpClientFactory
The following steps show how you can use Http retries with Polly integrated into IHttpClientFactory, which is explained in the previous section.
services.AddHttpClient<IBasketService, BasketService>() .SetHandlerLifetime(TimeSpan.FromMinutes(5)) //Set lifetime to five minutes .AddPolicyHandler(retryPolicy); // HERE }