Just built a simple retry engine in C#.

/// <summary>
/// Retry engine.
/// </summary>
public class RetryEngine
{
    private static Random rnd = new Random();
    private readonly ILogger<RetryEngine> logger;

    /// <summary>
    /// Creates new retry engine.
    /// </summary>
    /// <param name="logger">Logger</param>
    public RetryEngine(ILogger<RetryEngine> logger)
    {
        this.logger = logger;
    }

    /// <summary>
    /// Run a task with retry.
    /// </summary>
    /// <typeparam name="T">Response type.</typeparam>
    /// <param name="taskFactory">Task factory.</param>
    /// <param name="attempts">Retry times.</param>
    /// <param name="when">On error event.</param>
    /// <param name="timeOutSeconds">Timeout in seconds.</param>
    /// <returns>Response</returns>
    public async Task<T> RunWithTry<T>(
        Func<int, Task<T>> taskFactory,
        int attempts = 3,
        Predicate<Exception>? when = null,
        int timeOutSeconds = 300)
    {
        for (var i = 1; i <= attempts; i++)
        {
            try
            {
                this.logger.LogTrace($"Starting a job with retry. Attempt: {i}. (Starts from 1)");
                var workJob = taskFactory(i);
                var waitJob = Task.Delay(TimeSpan.FromSeconds(timeOutSeconds));
                await Task.WhenAny(workJob, waitJob);
                if (workJob.IsCompleted)
                {
                    return await workJob;
                }
                else
                {
                    throw new TimeoutException($"Job with cert access has exceeds the {timeOutSeconds} seconds timeout and we have to crash it to trigger another attempt.");
                }
            }
            catch (Exception e)
            {
                if (when != null)
                {
                    var shouldRetry = when.Invoke(e);
                    if (!shouldRetry)
                    {
                        this.logger.LogTrace(e, $"A task that was asked to retry failed. But from the given condition is false, we gave up retry.");
                        throw;
                    }
                    else
                    {
                        this.logger.LogTrace(e, $"A task that was asked to retry failed. With given condition is true.");
                    }
                }

                if (i >= attempts)
                {
                    this.logger.LogCritical(e, $"A task that was asked to retry failed. Maximum attempts {attempts} already reached. We have to crash it.");
                    throw;
                }

                this.logger.LogInformation(e, $"A task that was asked to retry failed. Current attempt is {i}. maximum attempts is {attempts}. Will retry soon...");

                await Task.Delay(ExponentialBackoffTimeSlot(i));
            }
        }

        throw new InvalidOperationException("Code shall not reach here.");
    }

    /// <summary>
    /// Please see <see href="https://en.wikipedia.org/wiki/Exponential_backoff">Exponetial backoff </see> time slot. 
    /// </summary>
    /// <param name="time">the time of trial</param>
    /// <returns>Time slot to wait.</returns>
    private static TimeSpan ExponentialBackoffTimeSlot(int time)
    {
        var max = (int)Math.Pow(2, time);
        return TimeSpan.FromSeconds(rnd.Next(0, max));
    }
}

When you have this, you can do in your business code:

this.retryEngine.RunWithTry(attempt =>
{
    return dmsClient.ExecuteManagementCmdlet(cmdletName, parameters);
}, when: e => e is WebException, attempts: 3);