More Resilient Service-to-Service Communication with Polly and .NET HttpClientFactory

May 30, 2019
Written by
Bryan Hogan
Contributor
Opinions expressed by Twilio contributors are their own

more-resilient-service-to-service-to-service-communication-polly-dotnet.png

Using Polly, the resilience framework for .NET, you can gracefully handle the lost packets, thrown exceptions, and failed requests which inevitably make their way into service-to-service communications on the web. A previous post on the Twilio blog introduced Polly, the .NET resilience framework. This post builds on that knowledge to strengthen your application's interaction with API endpoints.

The original .NET HttpClient was a convenient way to send and receive HTTP requests and responses on the web, but unless HttpClient was carefully implemented its use could result in socket exhaustion. The release of .NET Core 2.1 introduced HttpClientFactory, which can be instantiated once and used throughout the life of an application.

This post will show you how to combine HttpClientFactory with Polly to achieve efficient and resilient service-to-service communications. You'll see how you can do it more reliably, conveniently, and with less code than using HttpClient.

Prerequisites

To follow along with this post you need:

.NET Core 2.1+ SDK (The SDK includes the runtime.)

Git (Used to clone the repository for this post.)

curl, Fiddler, Postman, or Insomnia (The Git installer for Windows includes a curl executable.)

Visual Studio 2017/2019, Visual Studio Code, or your favorite development environment.

The companion repository for this post is available on GitHub. You'll use it as the basis for the coding you'll do in the tutorial that follows.

Getting started

The companion repository contains two Visual Studio ASP.NET Core 2.1 Web API solutions, one implementing a Weather Service and the other a Temperature Service. The Weather Service has three action methods, one to get the weather information for a location, one to update weather information for a location and one to delete weather information for a location. The Weather Service in turn calls a highly unreliable Temperature Service. You can imagine that the Weather Service would also call other backend services like a humidity service, a storm tracker service, etc., but only the Temperature Service is shown in this example.  

You'll run the TemperatureService project to test the code, but all the changes you'll need to make to implement HttpClientFactory with Polly will be made to the WeatherService project.

Check out the code in TemperatureController.cs to see how the unreliable Temperature Service is implemented. In TemperatureController.cs all the methods return failures 75% of the time.

Under the PollyHttpClientFactoryWeatherService project directory, open the following solution files in separate instances of Visual Studio:

TemperatureService/TemperatureService.sln
WeatherService/WeatherService.sln

Using separate instances of Visual Studio (or separate console windows if you're running from the .NET CLI) will enable you to conveniently run both services simultaneously and watch the console output from each application.

In the WeatherService project, open the WeatherController.cs file. It has three methods that correspond to GET, POST, and DELETE HTTP methods. They call corresponding methods in the TemperatureController.cs file in the TemperatureServiceproject.

public class WeatherController : ControllerBase
{
        private readonly HttpClient _httpClient;

        public WeatherController(HttpClient httpClient)
        {
                _httpClient = httpClient;
        }

        [HttpGet("{locationId}")]
        public async Task<ActionResult> Get(int locationId)
        {
                HttpResponseMessage httpResponseMessage = await _httpClient.GetAsync($"temperature/{locationId}");

                if (httpResponseMessage.IsSuccessStatusCode)
                {
                        int temperature = await httpResponseMessage.Content.ReadAsAsync<int>();
                        return Ok(temperature);
                }

                return StatusCode((int)httpResponseMessage.StatusCode, "The temperature service returned an error.");
        }

        [HttpPost]
        public async Task<ActionResult> Post([FromBody] WeatherInfo weatherModel) 
        {
                var temperatureInfo = new TemperatureInfo
                {
                        LocationId = weatherModel.LocationId,
                        Temperature = weatherModel.Temperature,
                        DateMeasured = weatherModel.DateTemperatureMeasured
                };

                string temperatureJson = JsonConvert.SerializeObject(temperatureInfo);
                HttpContent httpContent = new StringContent(temperatureJson, Encoding.UTF8, "application/json");

                var httpResponseMessage = await _httpClient.PostAsync("temperature", httpContent);

                return StatusCode((int) httpResponseMessage.StatusCode);
        }

        [HttpDelete("{locationId}")]
        public async Task<ActionResult> Delete(int locationId)
        {
                HttpResponseMessage httpResponseMessage = await _httpClient.DeleteAsync($"temperature/{locationId}");

                return StatusCode((int)httpResponseMessage.StatusCode);
        }
}

Note how the HttpClient is passed into the controller class through dependency injection in the constructor. HttpClient will change to HttpClientFactory in later modifications to the code.

Test the broken services

Try out the existing, unmodified code to see the Weather Service returning HTTP errors based on the errors it receives from the Temperature Service. The unmodified code doesn't include Polly, so it doesn't have the resilience features that would handle these failures automatically.

Start the two services from their separate instances of Visual Studio (or from the .NET CLI if you're working with another IDE or an editor). The Weather Service opens on HTTP port 5001 and the Temperature Service opens on port 6001.

The following instructions for making the HTTP calls to the Weather Service use curl, but you can certainly use Fiddler, Postman, Insomnia, or PowerShell Invoke-WebRequest if you prefer.

Open a console window. If you're a Windows user, you should use a Windows Command Prompt window rather than a PowerShell window unless you're familiar with the configuration of curl and PowerShell on your system.  Depending on your environment, you may have to adjust the syntax slightly to get them to work for you.

Try the HTTP GET request first.

Execute the following command-line instruction in your console window:

curl -i http://localhost:5001/weather/11

Your request will first hit the Weather Service, which then makes a request to the Temperature Service. The call to the Temperature Service will result in an HTTP 500 Internal Server Error response being returned to the Weather Service.

Execute the command three more times. You will get two more 500 error responses and the fourth time you'll get an HTTP 200 OK response along with an integer for temperature returned from the Temperature Service. If you're using a graphical tool to make the calls you'll see the temperature value in the Content field.

Here’s an example of how to make the same request in Fiddler: open the composer tab, enter http://localhost:5001/weather/11 in the address bar, make sure GET is selected in the dropdown list, and hit Execute four times. Your session should look like the following:

Fiddler Web Debugger

Try the HTTP DELETE method. Execute the following command-line instruction four times:

curl -i -X DELETE  http://localhost:5001/weather/11

The first three times you execute the command it will return a 500 Internal Server Error and the fourth time will return a 200 OK response.

Using the POST is a little more involved. You have to set the Content-Type header to application/json and provide a body for the request, as follows:

curl -i -X POST  -H "Content-Type: application/json" -d "{\"locationId\": 11,\"temperature\": 99,\"DateTemperatureMeasured\": \"2001-01-01\"}" http://localhost:5001/weather/`

The first three calls to this will return 500 errors and the fourth will return a 201 Created response.

To execute the DELETE method use the following command:

curl -i --request DELETE  http://localhost:5001/weather/11

Again, the first three instances will fail with 500 Internal Server Errors and the fourth will succeed with a 200 OK.

Leave the console window (or application) open. You'll be using the same commands to test the Polly and HttpClientFactory functionality later on.

Adding HttpClientFactory and Polly Policies

Enough with broken code, let's fix it all. (Well, mostly, but you'll come to that later.)

If you don’t want to code along, execute the following command in the PollyHttpClientFactoryWeatherService directory to checkout the finished code:


git checkout Polly_HttpClientFactory_End

There will be changes to three files in the WeatherService project: WeatherService.csproj, Startup.cs, and WeatherController.cs.

If you're coding along with these instructions, here's an overview of what your going to do:

In the Startup class for the Weather Service project you are going to define the Polly policies, the HttpClientFactory that returns HTTP clients, and a policy selector to choose the right policy based on the request.

At the point you use Polly to make a request in the controller methods your code will go from this: 

HttpResponseMessage httpResponseMessage = await _retryPolicy.ExecuteAsync(() =>
   _httpClient.GetAsync($"temperature/{locationId}"));

To this:

HttpResponseMessage httpResponseMessage = await   _httpClient.GetAsync($"temperature/{locationId}"));

You can't even tell Polly is there! Read on to find out how. 

In the WeatherService project, add the Microsoft.Extensions.Http.Polly NuGet package, being sure to pick the version that works with the version of .NET Core you are using. The source code provided in the companion repository uses .NET Core 2.1, so the appropriate version of the Polly NuGet package is  version 2.1.1.

In Startup.cs add a using Polly; statement at the top of the file.

Add three Polly policies to the ConfigureServices method by inserting the following C# code immediately before the end of the method:

IAsyncPolicy<HttpResponseMessage> httpRetryPolicy =
                Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
                    .RetryAsync(3);

IAsyncPolicy<HttpResponseMessage> httpWaitAndRetryPolicy =
        Policy.HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
                .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(retryAttempt));

IAsyncPolicy<HttpResponseMessage> noOpPolicy = Policy.NoOpAsync()
        .AsAsyncPolicy<HttpResponseMessage>();

The first policy retries failed requests immediately and up to three times.

The second policy retries failed requests up to three times, but adds a delay between each retry.

The final policy does nothing: it lets the request and response pass right through it without affecting them in any way. This one will be used for POST calls, because POST calls are not idempotent and you should not retry a failed request in all scenarios.

Adding HttpClientFactory is very easy: in this case you'll be adding a named HttpClient (as opposed to a typed one), setting the base address, and adding a single header.

Add the following C# code immediately after the code you added in the previous step:

services.AddHttpClient("TemperatureService", client =>
{
        client.BaseAddress = new Uri("http://localhost:6001/");
        client.DefaultRequestHeaders.Add("Accept", "application/json");
}) //note I have not put a ; at the end of the line.

Along with injection an instance of the HttpClient class into a controller, HttpClientFactory allows us to select a Polly policy to apply to a given request.

The code below selects the polices defined above based on the HTTP verb, but you can use any criteria that suits, another popular one is the URL of the request.

Add the following C# code at the end of the line of the previous code block immediately following the closing parenthesis ( ) ) and overwriting the comment:

  .AddPolicyHandler(httpRequestMessage =>
{
    if (httpRequestMessage.Method == HttpMethod.Get)
    {
        return httpWaitAndRetryPolicy;
    }

    if (httpRequestMessage.Method == HttpMethod.Post)
    {
        return noOpPolicy;
    }

    return httpRetryPolicy;
});

That’s almost everything fixed, except for a few minor changes to WeatherController.cs.

Remove the _httpClient private field and change the constructor to take an instance of HttpClientFactory as an argument by replacing the existing code with the following:

private readonly IHttpClientFactory _httpClientFactory;

public WeatherController(IHttpClientFactory httpClientFactory)
{
        _httpClientFactory = httpClientFactory;
}

Inside the GET method, remove the line that references the _httpClient. Instead, use the HttpClientFactory to request a HttpClient and then make the request to the Temperature Service. Note how there is no reference to Polly or the policies; there isn't even a using statement!

var httpClient = _httpClientFactory.CreateClient("TemperatureService");

HttpResponseMessage httpResponseMessage = await httpClient.GetAsync($"temperature/{locationId}");

To fix up the POST method add the following line to the top of the method:

var httpClient = _httpClientFactory.CreateClient("TemperatureService");

Locate the call to the Temperature Service shown in the following line of code:

var httpResponseMessage = await _httpClient.PostAsync("temperature", httpContent);

Replace the code above with the following:

var httpResponseMessage = await httpClient.PostAsync("temperature", httpContent);

For the DELETE method, add the following C# code to the beginning of the method:

var httpClient = _httpClientFactory.CreateClient("TemperatureService");

Find the existing call to the Temperature Service shown below:

HttpResponseMessage httpResponseMessage = await _httpClient.DeleteAsync($"temperature/{locationId}");

Replace the code above with the following C# code:

HttpResponseMessage httpResponseMessage = await httpClient.DeleteAsync($"temperature/{locationId}");

That’s it, you’re done. Try it out!

Testing Polly with HttpClientFactory

You can see the new code in action by repeating the same curl commands you used to see what happens with "brittle" service-to-service communication. This time your Weather Service is built to handle failed requests to the unreliable Temperature Service.

When you try HTTP GET and DELETE requests to the Weather Service you will get success responses because the HttpClientFactory is applying the Polly Retry and Wait & Retry policies to the requests. You'll see there is a noticeable delay in the GET responses because the Polly Wait & Retry policy configured in Startup.cs specifies an interval before each call is repeated. With three retries, that adds up to a perceptible delay. Wait & Retry is a useful strategy when calling services that experience transient spikes in utilization; waiting allows time for the service to be able to process requests.

If you try the POST you will see the same failure rate because the Polly NoOpAsync (No Operation) policy is being used. Polly doesn't take any action when such requests fail.

If you have built a new .NET Core 2.1 or 2.2 application without Polly—but with HttpClientFactory—you can see how easy it would be to retrofit. Make all your changes in the ConfigureServices method and your done.

Summary

With the arrival of the HttpClientFactory in .NET Core 2.1, using Polly has become even easier; resilience policies are applied to requests from the HttpClient through the dependency injection and the right policy can be chosen for a given request with a policy selector. All code is isolated in the Startup class and you even don’t need to reference Polly where you make the HTTP requests!

Additional Resources

Companion Repository – The complete code for the projects in this post is available on GitHub and can be reused under the MIT license.

Bryan’s blog posts on Polly - Check out some of Bryan’s posts on Polly on his own blog.

The Polly Project – The project homepage is an essential resource for new feature announcements and other Polly news. Check out the elevator pitch while you’re there.

The Polly repo on GitHub – The source, issues, and essential usage instructions.

Polly in the NuGet Gallery – All the installation goodness.

Bryan Hogan is a software architect, podcaster, blogger, Pluralisight author, and speaker who has been working in .NET since 2004. He lives in the Boston area and is involved with the community there through meetups and other events. Last year Bryan authored a Pluralsight course on Polly, he also blogs at the No Dogma Blog and even hosts a podcast!