Building Resilient APIs in .NET 8 Using Microsoft’s Built-in Tools and NuGet Packages
In .NET 8, Microsoft introduced the Microsoft.Extensions.Resilience
library to empower developers in creating robust applications, particularly in managing transient faults like network disruptions or temporary service outages. This library integrates seamlessly with Microsoft.Extensions.Http.Resilience
and builds upon the widely-used Polly library.
We discuss more detailed example of using Microsoft.Extensions.Http.Resilience
in .NET 8 to build a resilient HTTP client with retry, circuit breaker and timeout policies.
Following are steps to integrate with our sample .NET8 application :
1. Create ASP.NET Core Web API named TestWebAPI
2. Install Required Packages
First, install the Microsoft.Extensions.Http.Resilience
package:
see Preview Changes:
Here we can see, The Microsoft.Extensions.Http.Resilience
library is built on Polly, a trusted .NET library for managing resilience and transient faults. Polly empowers developers to define robust resilience strategies such as retries, circuit breakers, timeouts, rate limiting, fallbacks, and hedging, simplifying the process of handling potential disruptions in applications.
3. Create an external Web API named ThirdPartyAPI
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var app = builder.Build();
// Configure the HTTP request pipeline.
app.MapGet("/testapi", () =>
{
return "test response";
});
app.Run();
4. Set Up a Resilience Pipeline in Program.cs (under TestWebAPI)
In your Program.cs
or wherever you configure services, define a resilience pipeline with retry and timeout strategies using the AddResiliencePipeline
extension method. This setup will ensure that each HTTP request has required resilience strategies.
There are many resilience strategies available out of the box, allowing us to choose the most suitable one based on our use case.
builder.Services.AddResiliencePipeline("default", builder =>
{
builder.AddRetry(new RetryStrategyOptions
{
ShouldHandle = new PredicateBuilder().Handle<TimeoutRejectedException>(),
// Retry delay
Delay = TimeSpan.FromSeconds(2),
// Maximum retries
MaxRetryAttempts = 3,
// Exponential backoff
BackoffType = DelayBackoffType.Constant,
// Adds jitter to reduce collision
UseJitter = true
})
.AddCircuitBreaker(new CircuitBreakerStrategyOptions
{
// Customize and configure the circuit breaker logic.
SamplingDuration = TimeSpan.FromSeconds(3),
FailureRatio = 0.7,
MinimumThroughput = 2,
ShouldHandle = static args =>
{
return ValueTask.FromResult(args is
{
Outcome.Result : HttpStatusCode.RequestTimeout or HttpStatusCode.TooManyRequests
});
}
})
// Timeout after 15 seconds
.AddTimeout(TimeSpan.FromSeconds(15));
});
var app = builder.Build();
The order of resilience strategies matters, so we should apply them in the recommended sequence.
Explanation of Resilience Strategies
- Retry Strategy: Automatically retries failed requests based on the provided conditions, which can help handle temporary network issues without manual intervention. Configured with exponential backoff, it will retry the request up to three times with a two second delay. Adding jitter (a small random delay) helps avoid all requests retrying at once, reducing server overload.
- Circuit Breaker Strategy: The circuit breaker blocks the execution if too many direct failures or timeouts are detected. Adds a circuit breaker strategy with a sampling duration of 3 seconds, a failure ratio of 0.7 (70%), a minimum throughput of two, and a predicate that handles
RequestTimeout
andTooManyRequests
HTTP status codes to the resilience builder. - Timeout Strategy: Prevents long waits on unresponsive services by limiting how long an operation can run before failing with a timeout. Cancels the request if it takes longer than 15 seconds, throwing a
TimeoutRejectedException
.
5. Inject and Use Resilience Pipeline in a Service (under TestWebAPI)
Next, inject the resilience pipeline into a service. This example shows how to use it within a service class that calls an external API.
using Polly.Registry;
using System.Net.Http;
using System.Threading.Tasks;
public class ExternalService : IExternalService
{
private readonly HttpClient _httpClient;
private readonly ResiliencePipelineProvider<string> _resiliencePipelineProvider;
public ExternalService(HttpClient httpClient, ResiliencePipelineProvider<string> resiliencePipelineProvider)
{
_httpClient = httpClient;
_resiliencePipelineProvider = resiliencePipelineProvider;
}
public async Task<string> GetMessageAsync()
{
// Get the configured pipeline
var pipeline = _resiliencePipelineProvider.GetPipeline("default");
// Call sample external API
var response = await pipeline.ExecuteAsync(async ct =>
await _httpClient.GetAsync("http://localhost:5254/testapi/", ct)
);
return await response.Content.ReadAsStringAsync();
}
}
Here, The TestWebAPI is invoking the testapi
operation of the third-party/external service(ThirdPartyAPI i.e. http://localhost:5254).
GetMessageAsync
retrieves the configured pipeline using a unique identifier ("default"
) and then calls ExecuteAsync
. This method applies the resilience policies (retry, circuit breaker and timeout) to the HTTP request.
6. Map Endpoint in Program.cs (under TestWebAPI)
Finally, map an endpoint in Program.cs
to use this resilient service:
app.MapGet("/message", async (ExternalService externalService) =>
{
var result = await externalService.GetMessageAsync();
return result;
})
.WithOpenApi();
When clients access the /message
endpoint, the ExternalService
calls the external API using the resilient HTTP client configured with retries and timeouts.
7. Running Two Applications Locally
The following settings are required to run both the external API(ThirdPartyAPI) and the consumer app (TestWebAPI) simultaneously.
After running both applications, We observed that the system works as expected:
Note: For testing purposes, introducing a 4-second delay in the third-party/external API call.
// Configure the HTTP request pipeline.
app.MapGet("/testapi", () =>
{
//For testing purposes, introducing a 4-second delay.
Task.Delay(4000).Wait();
return "test response";
});
In TestWebAPI application, A Circuit Breaker pattern helps prevent cascading failures by monitoring external API calls and “breaking” the connection when a failure threshold is exceeded.
The operation didn’t complete within the allowed timeout of ‘00:00:03’.
This example is particularly useful in distributed systems where requests to external services may occasionally fail due to transient issues, helping applications remain responsive and fault-tolerant.
Benefits of a Common Resilience Pipeline:
- Reusability: You can easily reuse the same resilience pipeline across various APIs, keeping the code DRY (Don’t Repeat Yourself).
- Flexibility: You can modify the resilience pipeline (e.g., change retry count, timeout duration) in one central place.
- Consistency: Having a common resilience strategy applied universally ensures consistent error handling, retry logic, and failover behavior across different external services.
- Maintainability: If you need to update your error handling policies or add new ones, you only need to change the pipeline in one place.
As a best practice, it is recommended to create a common resilience pipeline that can be applied universally based on specific needs as mentioned in the above example. However, for more granular control, resilience can also be applied to a particular API by using the AddStandardResilienceHandler
extension method on an IHttpClientBuilder
instance.
AddStandardResilienceHandler(IHttpClientBuilder) Adds a standard resilience handler that uses multiple resilience strategies with default options to send the requests and handle any transient errors.
If you look closely at the definition of AddStandardResilienceHandler
, it incorporates multiple resilience strategies.
Additional Resilience Strategies
Other resilience strategies supported by Microsoft.Extensions.Http.Resilience
include:
- Fallback: Allows specifying a default value or alternative response in case of failure.
- Rate Limiting: Limits the number of requests over a time period to prevent overloading external services.
- Concurrency Limiter: Limit how many requests you make, which enables you to control outbound load.
- Hedging : Issue multiple requests in case of high latency or failure, which can improve responsiveness.
Thanks for reading.