Search code examples
c#dotnet-httpclientfallbackpollyretry-logic

Polly patterns in C# for a workflow?


I have a workflow like this:

  • call an API method
    • if any exception is thrown but timeout, the program logs that exception and throws it.
  • if timeout is thrown, the program has to call another API method.
    • after calling another API method, if everything goes true, the program returns a result.
    • otherwise the program throws an exception and log it.

both API methods have the same type of result. I want to implement this with polly policies.

This is my sample code:

var retryPolicy = Policy
    .Handle<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.RequestTimeout)
    .RetryAsync(1, async (exception, retryCount) =>
        await CallAnotherAPI());

var fallbackPolicy = Policy<HttpResponseMessage>
    .Handle<Exception>()
    .FallbackAsync((r, c, ct) => throw r.Exception,
    async (r, c) =>
    {
        Log(r.Message);
    });

var result = await fallbackPolicy
    .WrapAsync(retryPolicy)
    .ExecuteAsync(async () =>
    {
        await CallAPI();
    });

but it doesn't work and all the time fallbackPolicy is executed. How can I write the code that if retryPolicy goes true, fallbackPolicy won't be executed?


Solution

  • If I understand your workflow correctly then you don't need the retry policy at all. The Fallback policy is enough for this.

    So, let suppose that the CallApi and CallAnotherApi are implemented like this:

    private static HttpClient client = new HttpClient();
    public static async Task<HttpResponseMessage> CallAPI()
    {
        return await client.GetAsync("http://httpstat.us//408");
    }
    
    public static async Task<HttpResponseMessage> CallAnotherAPI()
    {
        var response = await client.GetAsync("http://httpstat.us//500");
        response.EnsureSuccessStatusCode();
        return response;
    }
    
    • I've used httpstatus.us to simulate certain http status codes
    • CallApi will always fail with Request Timeout
    • CallAnotherApi will always throw HttpRequestException because of the Ensure method call

    Now let's see the policy definition and usage:

    public static async Task Main(string[] args)
    {
        var fallbackPolicy = Policy<HttpResponseMessage>
        .HandleResult(msg => msg.StatusCode == HttpStatusCode.RequestTimeout)
        .FallbackAsync(async (_) => await CallAnotherAPI());
    
        HttpResponseMessage result;
        try
        {
            result = await fallbackPolicy
                .ExecuteAsync(async () =>
                {
                    return await CallAPI();
                });
        }
        catch (Exception ex) {
            Console.WriteLine(ex.Message); //TODO: replace with logging
            throw;
        }
    
        Console.WriteLine(result.StatusCode);
    }
    
    • The fallback policy should be triggered only if the response's status code was 408
    • ExecuteAsync will throw exception if either the CallApi or CallAnotherApi throws

    Let's see the different scenarios one-by-one

    CallApi succeeds

    public static async Task<HttpResponseMessage> CallAPI()
    {
        return await client.GetAsync("http://httpstat.us//200");
    }
    

    Output

    OK
    

    CallApi fails

    public static async Task<HttpResponseMessage> CallAPI()
    {
        var response = await client.GetAsync("http://httpstat.us//500");
        response.EnsureSuccessStatusCode();
        return response;
    }
    

    Output

    Response status code does not indicate success: 500 (Internal Server Error).
    

    Then the application crashes because of throw;

    CallApi timeouts and CallAnotherApi succeeds

    public static async Task<HttpResponseMessage> CallAPI()
    {
        return await client.GetAsync("http://httpstat.us//408");
    }
    
    public static async Task<HttpResponseMessage> CallAnotherAPI()
    {
        var response = await client.GetAsync("http://httpstat.us//200");
        response.EnsureSuccessStatusCode();
        return response;
    }
    

    Output

    OK
    

    CallApi timeouts and CallAnotherApi fails

    public static async Task<HttpResponseMessage> CallAPI()
    {
        return await client.GetAsync("http://httpstat.us//408");
    }
    
    public static async Task<HttpResponseMessage> CallAnotherAPI()
    {
        var response = await client.GetAsync("http://httpstat.us//500");
        response.EnsureSuccessStatusCode();
        return response;
    }
    

    Output

    Response status code does not indicate success: 500 (Internal Server Error).
    

    Then the application crashes because of throw;