-1

I'm trying to find an easy way to implement Polly on my GraphServiceClient using DI.

I have my GraphApiBuilder class that I use to interface with any class that needs the GraphServiceClient since injecting it directly in a class with hard logic would make it practically untestable:

public class GraphApiBuilder : IGraphApiBuilder, ISingletonDependency
{
    private readonly GraphServiceClient _graphServiceClient;

    public GraphApiBuilder(GraphServiceClient graphServiceClient)
    {
        _graphServiceClient = graphServiceClient;
    }

    public GraphServiceClient Create()
    {
        return _graphServiceClient;
    }
}

And in my ConfigureServices method, I do this:

context.Services.AddSingleton(_ =>
{
    var graphApiOptions = new GraphApiOptions();
    context.Services.GetConfiguration().Bind(GraphApiOptions.SectionName, graphApiOptions);
    var clientSecretCredential = new ClientSecretCredential(graphApiOptions.TenantId, graphApiOptions.ClientId, graphApiOptions.ClientSecret);
    return new GraphServiceClient(clientSecretCredential);
});

You can see that everything is singleton since Microsoft recommend it in this doc

Now I would like that when GraphServiceClient return an error, it uses Polly to retry. I don't want to duplicate this code.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • Why would you need to duplicate anything? Just wrap this client configuration in a polly proxy inside the `AddSingleton` – Wiktor Zychla Apr 19 '23 at 11:03
  • 2
    I'm unfamiliar with the graph api, but it seems like there is built-in support for retry via [`RetryHandler`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.graph.retryhandler?view=graph-core-dotnet) and [`RetryHandlerOption`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.graph.retryhandleroption?view=graph-core-dotnet). So, why do you need Polly? – Peter Csala Apr 19 '23 at 11:03

1 Answers1

0

After reading Peter Csala comment on my original post, I found out that Microsoft already include a RetryHandler by default on the GraphServiceClient.

However, I wanted to override the Retry mechanism with my own and I found this post showing a custom implementation that was a good starting point.

My final good looks like this:

    private const string RetryAfterHeaderKey = "Retry-After";
    private const int MaxRetry = 3;

   [...]

    context.Services.AddSingleton(_ =>
        {
            var graphApiOptions = new GraphApiOptions();
            context.Services.GetConfiguration().Bind(GraphApiOptions.SectionName, graphApiOptions);
            var clientSecretCredential = new ClientSecretCredential(graphApiOptions.TenantId, graphApiOptions.ClientId, graphApiOptions.ClientSecret);
            var handlers = GraphClientFactory.CreateDefaultHandlers(new TokenCredentialAuthProvider(clientSecretCredential));
            // Find the index of the existing RetryHandler, if any
            var retryHandlerIndex = handlers.FindIndex(handler => handler is RetryHandler);

            // Remove the existing RetryHandler, if found
            if (retryHandlerIndex >= 0)
            {
                handlers.RemoveAt(retryHandlerIndex);
                handlers.Insert(retryHandlerIndex, new RetryHandler(new RetryHandlerOption()
                {
                    MaxRetry = MaxRetry,
                    ShouldRetry = (delay, attempt, httpResponse) =>
                    {
                        if (httpResponse.IsSuccessStatusCode)
                        {
                            return false;
                        }

                        Log.Error($"{httpResponse.RequestMessage?.RequestUri} request returned status code {httpResponse.StatusCode}");
                        // Add more status codes here or change your if statement...
                        if (httpResponse?.StatusCode is HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden or HttpStatusCode.NotFound)
                        {
                            return false;
                        }

                        var delayInSeconds = CalculateDelay(httpResponse, attempt, delay);
                        switch (attempt)
                        {
                            case 0:
                                Log.Error($"Request failed, let's retry after a delay of {delayInSeconds} seconds");
                                break;
                            case MaxRetry:
                                Log.Error($"This was the last retry attempt {attempt}");
                                break;
                            default:
                                Log.Error($"This was retry attempt {attempt}, let's retry after a delay of {delayInSeconds} seconds");
                                break;
                        }

                        return true;
                    }
                }));
            }
            
            // Now we have an extra step of creating a HTTPClient passing in the customized pipeline
            var httpClient = GraphClientFactory.Create(handlers);

            // Then we construct the Graph Service Client using the HTTPClient
            return new GraphServiceClient(httpClient);
        });

internal static double CalculateDelay(HttpResponseMessage response, int retryCount, int delay)
    {
        var headers = response.Headers;
        double delayInSeconds = delay;
        if (headers.TryGetValues(RetryAfterHeaderKey, out var values))
        {
            var retryAfter = values.First();
            if (int.TryParse(retryAfter, out var delaySeconds))
            {
                delayInSeconds = delaySeconds;
            }
        }
        else
        {
            delayInSeconds = retryCount * delay;
        }
        const int maxDelay = 9;
        delayInSeconds = Math.Min(delayInSeconds, maxDelay);
        return delayInSeconds;
    }