Building resilient .NET applications with Polly - Part 7

In this brief concluding post, we will explore how it is possible to concurrently combine multiple strategies, thereby constructing highly resilient .NET applications.

Polly has introduced the concept of pipelines, and for each implemented strategy, we had to construct a pipeline. The term "pipeline" is not merely decorative; it explicitly suggests that policies can be amalgamated. Consequently, we can apply diverse strategies for each request, thus benefiting from the advantages of multiple approaches. In our scenario, we will implement a circuit breaker coupled with a fallback strategy.

What is a fallback strategy ?

A fallback strategy is a resilience pattern employed to handle failures gracefully by providing an alternative behavior or value when the primary operation encounters an issue. The purpose of a fallback strategy is to ensure that, even in the face of failures or errors, the system can continue to operate or provide a meaningful response. In the context of Polly (and other resilience frameworks anyway), a fallback strategy involves specifying a backup operation or value that should be executed or returned if the primary operation fails. This alternative behavior serves as a contingency plan, helping the system to avoid complete failure and allowing it to maintain partial functionality.

Implementing several strategies with Polly

In our case, we will need to define two fallback strategies: one for when the exception is caught by the circuit breaker and another for when it is caught by any other exception.

 1[FunctionName(nameof(GetAccountById04))]
 2public async Task<IActionResult> GetAccountById04([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
 3{
 4    var pipeline = FallbackCircuitBreakerSingleton.Instance.Pipeline;
 5
 6    // Execute the pipeline asynchronously
 7    var response = await pipeline.ExecuteAsync(async token =>
 8    {
 9        var client = new HttpClient();
10        var res = await client.GetAsync("http://localhost:7271/api/GetReturnAlwaysFailures", token).ConfigureAwait(false);
11
12        return res;
13
14    }).ConfigureAwait(false);
15
16    IActionResult result = response.StatusCode switch
17    {
18        HttpStatusCode.InternalServerError => new InternalServerErrorResult(),
19        HttpStatusCode.BadRequest => new BadRequestResult(),
20        HttpStatusCode.NoContent => new NoContentResult(),
21        _ => new OkResult()
22    };
23
24    return result;
25}
26
27public class FallbackCircuitBreakerSingleton
28{
29    private static FallbackCircuitBreakerSingleton _instance;
30
31    private CircuitBreakerStateProvider _provider;
32    private ResiliencePipeline<HttpResponseMessage> _pipeline;
33
34    private FallbackCircuitBreakerSingleton()
35    {
36        _provider = new CircuitBreakerStateProvider();
37
38        // Provide the return type (string) to be able to use Fallback.
39        var pipelineBuilder = new ResiliencePipelineBuilder<HttpResponseMessage>();
40
41        // Define a fallback strategy: provide a substitute message to the user, for any exception.
42        pipelineBuilder.AddFallback(new()
43        {
44            ShouldHandle = new PredicateBuilder<HttpResponseMessage>().HandleResult(r => r.StatusCode == HttpStatusCode.InternalServerError),
45            FallbackAction = args => Outcome.FromResultAsValueTask(new HttpResponseMessage(HttpStatusCode.BadRequest))
46        });
47
48        // Define a fallback strategy: provide a substitute message to the user, for BrokenCircuitException.
49        pipelineBuilder.AddFallback(new()
50        {
51            ShouldHandle = new PredicateBuilder<HttpResponseMessage>().Handle<BrokenCircuitException>(),
52            FallbackAction = args => Outcome.FromResultAsValueTask(new HttpResponseMessage(HttpStatusCode.NoContent))
53        });
54
55        pipelineBuilder.AddCircuitBreaker(new()
56        {
57            FailureRatio = 0.1,
58            SamplingDuration = TimeSpan.FromSeconds(30),
59            MinimumThroughput = 2,
60            BreakDuration = TimeSpan.FromSeconds(30),
61            ShouldHandle = new PredicateBuilder<HttpResponseMessage>().HandleResult(response => response.StatusCode == HttpStatusCode.InternalServerError),
62            StateProvider = _provider
63        });
64
65        _pipeline = pipelineBuilder.Build();
66    }
67
68    public static FallbackCircuitBreakerSingleton Instance
69    {
70        get
71        {
72            if (_instance == null) _instance = new FallbackCircuitBreakerSingleton();
73            return _instance;
74        }
75    }
76
77    public ResiliencePipeline<HttpResponseMessage> Pipeline
78    {
79        get
80        {
81            return _pipeline;
82        }
83    }
84
85    public CircuitState State
86    {
87        get
88        {
89            return _provider.CircuitState;
90        }
91    }
92}

Upon executing this request via Fiddler, it is apparent that these strategies are effectively combined. The initial requests fall back with errors 400 (as configured in the code), and once the circuit breaker is activated, they fall back with errors 204. Consequently, the application demonstrates resilience, effectively capturing failures in a service and preventing them from propagating throughout the entire system.

Final thoughts

If you wish to delve deeper into this topic, acquire the following book, which encompasses all the concepts emphasized in this series and delve into more advanced ones.

Release It!: Design and Deploy Production-Ready Software (Nygard)

Do not hesitate to contact me shoud you require further information.