How to use retry functionality in Microsoft Graph net SDK v5

A few months back I wrote a blockpost for how to use WithShouldRetry of the Microsoft Graph GraphServiceClient. This was a valid way to use the retry functionality for v4 of the SDK.

Well Microsoft updated there SDK to version 5 which uses the Kiota generator. If you want to know how to upgrade from v4 to v5 you can read all about it in the upgrade v4 to v5 documentation.

In v5 you don’t have fluent helper as WithMaxRetry or WithShouldRetry. I couldn’t find any documentation about it so I asked StackOverflow. Luckily I was not the only one. With some help, I found a way to maximize the retry functionality.

See a simple sample below that will try to get a random user and retries if it can’t be found. It won’t retry if the call is Unauthorized.

// using Microsoft.Kiota.Abstractions;
// using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;

// Use random Guid for user that doesn't exist to force retry
string userId = Guid.NewGuid().ToString();

const int MaxRetry = 5; // So number of call are MaxRetry + 1 (1 is the original call)

RetryHandlerOption retryHandlerOption = new RetryHandlerOption()
{
    MaxRetry = MaxRetry,
    ShouldRetry = (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}");
            return false;
        }
        else
            Console.WriteLine($"This was retry attempt {attempt}, let's retry after a delay of {delayInSeconds} seconds");

        return true;
    }
};

var requestOptions = new List<IRequestOption>
{
    retryHandlerOption,
};

User? user = await graphClient
    .Users[userId]
    .GetAsync(requestConfiguration => requestConfiguration.Options = requestOptions);


/// <summary>
/// This is reverse engineered from:
/// https://github.com/microsoftgraph/msgraph-sdk-dotnet-core/blob/dev2/src/Microsoft.Graph.Core/Requests/Middleware/RetryHandler.cs#L164
/// </summary>
/// <param name="response"></param>
/// <param name="retryCount"></param>
/// <param name="delay"></param>
/// <returns></returns>
private static double CalculateDelay(HttpResponseMessage response, int retryCount, int delay)
{
    HttpHeaders headers = response.Headers;
    double delayInSeconds = delay;
    if (headers.TryGetValues(RetryAfter, out var values))
    {
        var retryAfter = values.First();
        if (int.TryParse(retryAfter, out var delaySeconds))
        {
            delayInSeconds = delaySeconds;
        }
    }
    else
    {
        var mPow = Math.Pow(2, retryCount);
        delayInSeconds = mPow * delay;
    }

    const int
        MaxDelay = 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, MaxDelay);

    return delayInSeconds;
}

How to use WithShouldRetry of the Microsoft Graph GraphServiceClient

Update! See for v5 of the Microsoft Graph SDK the following blogpost.

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);
            }
        }
    }
}