Sheldon Cohen
4 min readJul 1, 2024

Building Resilient HTTP Apps using Microsoft.Extensions.Http.Resilience with .NET 8

Build resilient HTTP apps using .NET 8 and Microsoft.Extensions.Http.Resilience
Distributed Cloud

Introduction

In today’s distributed cloud environment, resilience isn’t just a nice-to-have — it’s a necessity. Applications that rely on external APIs must handle transient faults gracefully to maintain consistent performance and integrity. With .NET 8, Microsoft introduces Microsoft.Extensions.Http.Resilience, a built-in pipeline builder that simplifies creating resilient HTTP applications built on the Polly framework.

Let’s build a simple, resilient HTTP app using Microsoft.Extensions.Http.Resilience. We’ll create a minimal API endpoint that retrieves weather data from the OpenWeatherMap API, configuring resilience directly within the HTTP client. We’ll also use Route Grouping for better organization and integrate Swagger for API documentation.

Prerequisites

Setting Up the Project

  1. Create a new minimal API project:
// Command to create a new minimal API project
dotnet new web -n WeatherApi
cd WeatherApi

2. Add a solution file and put the project into it:

dotnet new sln
dotnet sln .\WeatherApi.sln add .\WeatherApi.csproj

3. Add necessary packages:

// Command to add necessary packages
dotnet add package Microsoft.Extensions.Http
dotnet add package Swashbuckle.AspNetCore

Implementing the Resilience Pipeline and Swagger Integration

Program.cs Configuration:

Here, we configure the HTTP client with a resilience strategy named “MyResilienceStrategy.” This includes retry, timeout, and circuit breaker policies. We also add and configure Swagger services for API documentation, setting Swagger UI as the default page.

using Microsoft.Extensions.Http.Resilience;
using Polly;
using WeatherApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpClient("WeatherClient", client =>
{
// Sets base URL for the weather API
client.BaseAddress = new Uri("https://api.openweathermap.org");
})
.AddResilienceHandler("MyResilienceStrategy", resilienceBuilder => // Adds resilience policy named "MyResilienceStrategy"
{
// Retry Strategy configuration
resilienceBuilder.AddRetry(new HttpRetryStrategyOptions // Configures retry behavior
{
MaxRetryAttempts = 4, // Maximum retries before throwing an exception (default: 3)

Delay = TimeSpan.FromSeconds(2), // Delay between retries (default: varies by strategy)

BackoffType = DelayBackoffType.Exponential, // Exponential backoff for increasing delays (default)

UseJitter = true, // Adds random jitter to delay for better distribution (default: false)

ShouldHandle = new PredicateBuilder<HttpResponseMessage>() // Defines exceptions to trigger retries
.Handle<HttpRequestException>() // Includes any HttpRequestException
.HandleResult(response => !response.IsSuccessStatusCode) // Includes non-successful responses
});

// Timeout Strategy configuration
resilienceBuilder.AddTimeout(TimeSpan.FromSeconds(5)); // Sets a timeout limit for requests (throws TimeoutRejectedException)

// Circuit Breaker Strategy configuration
resilienceBuilder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions // Configures circuit breaker behavior
{
// Tracks failures within this time frame
SamplingDuration = TimeSpan.FromSeconds(10),

// Trips the circuit if failure ratio exceeds this within sampling duration (20% failures allowed)
FailureRatio = 0.2,

// Requires at least this many successful requests within sampling duration to reset
MinimumThroughput = 3,

// How long the circuit stays open after tripping
BreakDuration = TimeSpan.FromSeconds(1),

// Defines exceptions to trip the circuit breaker
ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
.Handle<HttpRequestException>() // Includes any HttpRequestException
.HandleResult(response => !response.IsSuccessStatusCode) // Includes non-successful responses
});
});

// Add Swagger services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Load configuration
var configuration = builder.Configuration;
var appId = configuration["OpenWeatherMap:AppId"];

var app = builder.Build();

// Enable Swagger middleware
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Weather API V1");
options.RoutePrefix = string.Empty;
});

// Map routes using static map
app.MapWeatherRoutes(appId);

app.Run();

Abbreviated appsettings.json:

{
"OpenWeatherMap": {
"AppId": "YOUR_KEY_HERE"
}
}

RouteBuilderExtension.cs:

In this class, we define route grouping and our GetWeatherAsync endpoint. This helps keep our code organized and clean.

namespace WeatherApi;

public static class RouteBuilderExtension
{
public static void MapWeatherRoutes(this IEndpointRouteBuilder app, string appId)
{
app.MapGet("api/getweatherasync", async (IHttpClientFactory httpClientFactory) =>
{
var httpClient = httpClientFactory.CreateClient("WeatherClient");
var response = await httpClient.GetFromJsonAsync<WeatherForecastRecord>($"/data/2.5/weather?lat=44.34&lon=10.99&appid={appId}");

if (response != null && response is WeatherForecastRecord)
{
return Results.Json(response);
}

return Results.Problem("Unable to retrieve weather data.");
})
.WithName("GetWeatherAsync")
.WithTags("Weather");
}
}

And we’ll use a record type for WeatherForecastRecord which provides immutability, value-based equality, concise syntax, and improved readability and maintainability.

public record WeatherForecastRecord(
Coord coord,
IReadOnlyList<Weather> weather,
string @base,
Main main,
int visibility,
Wind wind,
Clouds clouds,
int dt,
Sys sys,
int timezone,
int id,
string name,
int cod
);

public record Clouds(
int all
);

public record Coord(
double lon,
double lat
);

public record Main(
double temp,
double feels_like,
double temp_min,
double temp_max,
int pressure,
int humidity,
int sea_level,
int grnd_level
);

public record Sys(
int type,
int id,
string country,
int sunrise,
int sunset
);

public record Weather(
int id,
string main,
string description,
string icon
);

public record Wind(
double speed,
int deg,
double gust
);

Explanation

Retry Strategy:

Our HTTP client retries up to 4 times with a 2-second interval between retries. The ShouldHandle predicate deals with HttpRequestException and unsuccessful status codes.

Timeout Strategy:

Each HTTP request has a 5-second timeout, ensuring our application doesn’t hang on slow responses.

Circuit Breaker Strategy:

If 20% of requests fail within a 10-second sampling duration, the circuit breaks for 1 second, preventing overload on an already struggling system.

Swagger Integration:

Swagger services are added and configured for API documentation. Swagger UI is set up as the default page upon launch.

Minimal API Endpoint:

The GetWeatherAsync endpoint uses the resilient HttpClient to call the OpenWeatherMap API, returning weather data or an error message if the call fails.

Running the Application

Open Windows Terminal, navigate to the project directory, and run the application:

// Command to run the application
dotnet run

Access the Swagger UI by opening your browser and navigating to http://localhost:7070. You should see the Swagger UI displaying the GetWeatherAsync endpoint under the Weather tag.

Conclusion

In today’s distributed cloud environment, building resilient HTTP applications is crucial for maintaining reliability and performance amid transient faults and network issues.

With Microsoft.Extensions.Http.Resilience, .NET developers can easily configure retries, timeouts, and circuit breakers, enhancing their applications’ resilience. By following the steps outlined in this article, you can ensure that your API endpoints remain robust and reliable, providing a better user experience. Additionally, integrating Swagger helps in documenting and testing your API endpoints efficiently.

See the full source code on GitHub

Sheldon Cohen

Technology professional with 15+ years of software development