How to use WithShouldRetry of the Microsoft Graph GraphServiceClient

I had some difficulty in to understand how WithShouldRetry method works of the Microsoft Graph .NET SDK. And I was not the only one because I saw several StackOverflow posts and an important issue about the retry. There is no documentation about this, so I thought it’s better to update a sample application that I have were I can test Microsoft Graph calls in C# with the dotnet SDK.

The sample application contains the method DisplayUserInfoAsync. This method is just printing the information of a user by his identifier. The complete class is defined at the bottom of this blogpost, but the important part is this:

User user = await graphClient.Users[userId]
.Request()
.WithMaxRetry(MaxRetry)
.WithShouldRetry((delay, attempt, httpResponse) =>
{
    Console.WriteLine($"Request returned status code {httpResponse.StatusCode}");

    // Add more status codes here or change your if statement...
    if (httpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        return false;

    double delayInSeconds = CalculateDelay(httpResponse, attempt, delay);

    if (attempt == 0)
        Console.WriteLine($"Request failed, let's retry after a delay of {delayInSeconds} seconds");
    else if (attempt == MaxRetry)
        Console.WriteLine($"This was the last retry attempt {attempt}");
    else
        Console.WriteLine($"This was retry attempt {attempt}, let's retry after a delay of {delayInSeconds} seconds");

    return true;
})
.GetAsync();

It uses the WithMaxRetry and WithShouldRetry options to manipulate the retry mechanism of the standard RetryHandler of the SDK. The official documentation of this retry handler can be found here. The retry handler uses by default the default RetryHandlerOptions. With the above methods you have more control on how they work.

So, in my sample application it works as follows. I do a call to a user that doesn’t exist. This will result in a 404 not found result. Because I want more information about the number of retries, I log this to the console. The CalculateDelay method is the same code as in the RetryHandler of the SDK but it gives me more details in the log messages.

This results in the following console log if I try to search for a non-existing user with a max retry of 5 (default is 3).

Because this user really doesn’t exist, it will end up throwing a “tooManyRetries” code in the “Microsoft.Graph.ServiceException”.

Hopefully this will help people in how to use these methods and hopefully Microsoft will create some official documentation about this.

Complete code or check the sample application:

using Microsoft.Graph;
using MicrosoftGraphWithMsi.Helpers;
using System.Net.Http.Headers;

namespace MicrosoftGraphWithMsi.Graph
{
    internal static class Users
    {
        private const string RETRY_AFTER = "Retry-After";

        internal static async Task DisplayLoggedInUserInfoAsync(GraphServiceClient graphClient, bool writeJsonObjectsToOutput = true)
        {
            User user = await graphClient.Me
                            .Request()
                            .GetAsync();

            Console.WriteLine("Logged in user:");
            PrintUserInformation(user, writeJsonObjectsToOutput);
        }

        internal static async Task DisplayUserInfoAsync(GraphServiceClient graphClient, string userId, bool writeJsonObjectsToOutput = true)
        {
            const int MaxRetry = 5; // So number of call are (MaxRetry + 1)

            User user = await graphClient.Users[userId]
                            .Request()
                            .WithMaxRetry(MaxRetry)
                            .WithShouldRetry((delay, attempt, httpResponse) =>
                            {
                                Console.WriteLine($"Request returned status code {httpResponse.StatusCode}");

                                // Add more status codes here or change your if statement...
                                if (httpResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized)
                                    return false;

                                double delayInSeconds = CalculateDelay(httpResponse, attempt, delay);

                                if (attempt == 0)
                                    Console.WriteLine($"Request failed, let's retry after a delay of {delayInSeconds} seconds");
                                else if (attempt == MaxRetry)
                                    Console.WriteLine($"This was the last retry attempt {attempt}");
                                else
                                    Console.WriteLine($"This was retry attempt {attempt}, let's retry after a delay of {delayInSeconds} seconds");

                                return true;
                            })
                            .GetAsync();

            Console.WriteLine("User information:");
            PrintUserInformation(user, writeJsonObjectsToOutput);
        }

        /// <summary>
        /// This is reverse engineerd from:
        /// https://github.com/microsoftgraph/msgraph-sdk-dotnet-core/blob/dev/src/Microsoft.Graph.Core/Requests/Middleware/RetryHandler.cs#L164
        /// </summary>
        /// <param name="response"></param>
        /// <param name="retry_count"></param>
        /// <param name="delay"></param>
        /// <returns></returns>
        internal static double CalculateDelay(HttpResponseMessage response, int retry_count, int delay)
        {
            HttpHeaders headers = response.Headers;
            double delayInSeconds = delay;
            if (headers.TryGetValues(RETRY_AFTER, out IEnumerable<string> values))
            {
                string retry_after = values.First();
                if (int.TryParse(retry_after, out int delay_seconds))
                {
                    delayInSeconds = delay_seconds;
                }
            }
            else
            {
                var m_pow = Math.Pow(2, retry_count);
                delayInSeconds = m_pow * delay;
            }

            const int MAX_DELAY = 180; // From github code https://github.com/microsoftgraph/msgraph-sdk-dotnet-core/blob/2e43863e349b4b3ebe2e166c26e3afcc4a974365/src/Microsoft.Graph.Core/Requests/Middleware/Options/RetryHandlerOption.cs#L18
            delayInSeconds = Math.Min(delayInSeconds, MAX_DELAY);

            return delayInSeconds;
        }



        private static void PrintUserInformation(User user, bool writeJsonObjectsToOutput)
        {
            Console.WriteLine($"Displayname: {user.DisplayName}");

            if (writeJsonObjectsToOutput)
            {
                Console.WriteLine();
                Console.WriteLine("User in JSON:");
                string json = user.ToFormattedJson();
                Console.WriteLine(json);
            }
        }
    }
}