In distributed systems and microservices architectures, maintaining resilience and preventing cascading failures is paramount. Implementing the Circuit Breaker pattern is a proven strategy to achieve this, ensuring that your application can gracefully handle external service failures. In the context of a scalable C#.NET Core Web API, the Polly library serves as an excellent tool for implementing the Circuit Breaker pattern, providing comprehensive features for resilience and transient-fault handling.
Begin by installing the necessary NuGet packages. Polly is the core library required for implementing resilience patterns, including the Circuit Breaker.
Run the following commands in your Package Manager Console or terminal:
dotnet add package Polly
dotnet add package Microsoft.Extensions.Http.Polly
appsettings.json
Externalizing the circuit breaker configuration allows for easy adjustments without modifying the codebase. Add a dedicated section for circuit breaker settings in your appsettings.json
file:
{
"CircuitBreakerSettings": {
"FailureThreshold": 0.5, // 50% failure rate to trigger the breaker
"SamplingDurationInSeconds": 30, // Time window for monitoring
"MinimumThroughput": 10, // Minimum number of requests in the sampling duration
"DurationOfBreakInSeconds": 60 // Duration to keep the circuit open
}
}
Create a strong-typed configuration class to map the settings defined in appsettings.json
. This facilitates easy access and management of the circuit breaker parameters within your application.
public class CircuitBreakerSettings
{
public double FailureThreshold { get; set; }
public int SamplingDurationInSeconds { get; set; }
public int MinimumThroughput { get; set; }
public int DurationOfBreakInSeconds { get; set; }
}
Program.cs
Integrate the circuit breaker policy into your application's service container. This setup ensures that the policy is applied to all outgoing HTTP requests managed by HttpClient
.
using Microsoft.Extensions.Configuration;
using Polly;
using Polly.CircuitBreaker;
var builder = WebApplication.CreateBuilder(args);
// Bind CircuitBreakerSettings from appsettings.json
var circuitBreakerSettings = builder.Configuration
.GetSection("CircuitBreakerSettings")
.Get<CircuitBreakerSettings>();
// Define the circuit breaker policy
var circuitBreakerPolicy = Policy
.Handle<Exception>()
.AdvancedCircuitBreakerAsync(
failureThreshold: circuitBreakerSettings.FailureThreshold,
samplingDuration: TimeSpan.FromSeconds(circuitBreakerSettings.SamplingDurationInSeconds),
minimumThroughput: circuitBreakerSettings.MinimumThroughput,
durationOfBreak: TimeSpan.FromSeconds(circuitBreakerSettings.DurationOfBreakInSeconds)
);
// Register the circuit breaker policy for dependency injection
builder.Services.AddSingleton<AsyncCircuitBreakerPolicy>(circuitBreakerPolicy);
// Register HttpClient with the circuit breaker policy
builder.Services.AddHttpClient("ExternalServiceClient")
.AddPolicyHandler(circuitBreakerPolicy);
var app = builder.Build();
app.Run();
Inject the configured circuit breaker policy into your service to manage external HTTP calls. This ensures that any failures are appropriately handled, and the circuit breaker logic is applied.
public class ExternalService
{
private readonly HttpClient _httpClient;
private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy;
private readonly ILogger<ExternalService> _logger;
public ExternalService(HttpClient httpClient, AsyncCircuitBreakerPolicy circuitBreakerPolicy, ILogger<ExternalService> logger)
{
_httpClient = httpClient;
_circuitBreakerPolicy = circuitBreakerPolicy;
_logger = logger;
}
public async Task<string> GetDataAsync()
{
try
{
return await _circuitBreakerPolicy.ExecuteAsync(async () =>
{
var response = await _httpClient.GetAsync("https://external-service.com/api/data");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
});
}
catch (BrokenCircuitException)
{
_logger.LogWarning("Circuit breaker is open. External service is unavailable.");
return "Service is currently unavailable. Please try again later.";
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching data.");
return $"An error occurred: {ex.Message}";
}
}
}
Utilize the implemented service within your API controllers to ensure that all external calls benefit from the circuit breaker’s resilience.
[ApiController]
[Route("[controller]")]
public class DataController : ControllerBase
{
private readonly ExternalService _externalService;
public DataController(ExternalService externalService)
{
_externalService = externalService;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var data = await _externalService.GetDataAsync();
return Ok(data);
}
}
The Circuit Breaker operates in three distinct states to manage the flow of requests based on the health of the external service:
State | Description |
---|---|
Closed | The circuit is operating normally, allowing all requests to pass through. Failures are monitored, and if they exceed the configured threshold within the sampling duration, the circuit transitions to the Open state. |
Open | The circuit has detected a high rate of failures and is now short-circuiting requests, immediately returning errors without attempting to call the external service. After the DurationOfBreak elapses, the circuit transitions to the Half-Open state. |
Half-Open | A limited number of test requests are allowed to pass through. If these requests succeed, the circuit is deemed healthy and transitions back to the Closed state. If failures persist, it returns to the Open state. |
Proper testing and monitoring are essential to ensure that the circuit breaker behaves as expected under different failure scenarios.
Microsoft.Extensions.Diagnostics.HealthChecks
to continuously monitor the status of external dependencies and expose circuit breaker states.To further enhance the resilience and flexibility of your circuit breaker implementation, consider the following advanced configurations:
Integrating retry policies with circuit breakers can help mitigate transient faults without overwhelming the external service.
var retryPolicy = Policy
.Handle<HttpRequestException>()
.OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
builder.Services.AddHttpClient("ExternalServiceClient")
.AddPolicyHandler(retryPolicy)
.AddPolicyHandler(circuitBreakerPolicy);
This configuration attempts to retry failed requests up to three times with exponential backoff before the circuit breaker intervenes.
Providing fallback responses enhances user experience by offering alternative data or messages when the primary external service is unavailable.
var fallbackPolicy = Policy<HttpResponseMessage>
.Handle<BrokenCircuitException>()
.Or<HttpRequestException>()
.OrResult(r => !r.IsSuccessStatusCode)
.FallbackAsync(new HttpResponseMessage
{
Content = new StringContent("Fallback response due to service unavailability.")
});
builder.Services.AddHttpClient("ExternalServiceClient")
.AddPolicyHandler(fallbackPolicy)
.AddPolicyHandler(circuitBreakerPolicy);
This policy ensures that a predefined fallback response is returned when the circuit is open or when a request fails after all retry attempts.
Combine the Circuit Breaker pattern with Bulkhead Isolation to limit the number of concurrent calls to external services, preventing resource exhaustion and maintaining application stability.
var bulkheadPolicy = Policy.BulkheadAsync<HttpResponseMessage>(maxParallelization: 10, maxQueuingActions: 10);
builder.Services.AddHttpClient("ExternalServiceClient")
.AddPolicyHandler(bulkheadPolicy)
.AddPolicyHandler(circuitBreakerPolicy);
Implementing a Circuit Breaker in your scalable C#.NET Core Web API using the Polly library is a critical step towards building resilient and reliable applications. By effectively managing external service dependencies, you can prevent cascading failures, enhance user experience, and maintain system stability even in the face of unforeseen service disruptions. Adhering to best practices, such as dynamic configuration, comprehensive monitoring, and fallback strategies, further strengthens your application's resilience and scalability.
Embrace the Circuit Breaker pattern as part of your broader resilience strategy to ensure your application can gracefully handle failures and continue to deliver value to your users.