Search code examples
c#.net.net-4.8retry-logic

How to implement a generic retry pattern for methods with parameters


So far I implemented this:

public class Retrier
{
    /// <summary>
    /// Execute a method with no parameters multiple times with an interval
    /// </summary>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="function"></param>
    /// <param name="tryTimes"></param>
    /// <param name="interval"></param>
    /// <returns></returns>
    public static TResult Execute<TResult>(Func<TResult> function, int tryTimes, int interval)
    {
        for (int i = 0; i < tryTimes - 1; i++)
        {
            try
            {
                return function();
            }
            catch (Exception)
            {
                Thread.Sleep(interval);
            }
        }

        return function();
    }

    /// <summary>
    /// Execute a method with 1 parameter multiple times with an interval
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="function"></param>
    /// <param name="arg1"></param>
    /// <param name="tryTimes"></param>
    /// <param name="interval"></param>
    /// <returns></returns>
    public static TResult Execute<T, TResult>(Func<T, TResult> function, T arg1, int tryTimes, int interval)
    {
        for (int i = 0; i < tryTimes - 1; i++)
        {
            try
            {
                return function(arg1);
            }
            catch (Exception)
            {
                Thread.Sleep(interval);
            }
        }

        return function(arg1);
    }

    /// <summary>
    /// Execute a method with 2 parameters multiple times with an interval
    /// </summary>
    /// <typeparam name="T1"></typeparam>
    /// <typeparam name="T2"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="function"></param>
    /// <param name="arg1"></param>
    /// <param name="arg2"></param>
    /// <param name="tryTimes"></param>
    /// <param name="interval"></param>
    /// <returns></returns>
    public static TResult Execute<T1, T2, TResult>(Func<T1, T2, TResult> function, T1 arg1, T2 arg2, int tryTimes, int interval)
    {
        for (int i = 0; i < tryTimes - 1; i++)
        {
            try
            {
                return function(arg1, arg2);
            }
            catch (Exception)
            {
                Thread.Sleep(interval);
            }
        }

        return function(arg1, arg2);
    }
}

The problem is that it becomes difficult to maintain. When I have a method with more variables, I need to implement another Execute method. In case I need to change anything I have to change it in all methods.

So I wonder if there is a clean way to make it one method that will handle any number of parameters?


Solution

  • As you said your current design does not support n and n+1 parameters without code change.

    Here are two common practices that can come to rescue.

    Define variants upfront

    If you look at the Action and Func delegates then you can see there are multiple variants from no parameter till 16 parameters.

    Action variants

    You can follow the same concept in case of Execute. The beauty here is that you can generate this code via T4.

    Anticipate anonymous functions

    Rather than having handful of overloads you can cover all cases with this

    public static TResult Execute<TResult>(Func<TResult> function, int tryTimes, int interval)
    

    and all asynchronous cases with that:

    public static Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> function, int tryTimes, int interval)
    

    The trick here is that how you pass the parameters

    Execute(MyFunction);
    Execute(() => MyFunction()); //This is exactly the same as the above one
    Execute(() => MyOtherFunction(param1));
    
    ExecuteAsync(MyFunctionAsync);
    ExecuteAsync(() => MyFunctionAsync()); //This works if `ExecuteAsync` awaits `function` 
    ExecuteAsync(async () => await MyOtherFunctionAsync(param1));
    

    So, with the following four methods you can cover all methods and functions regardless they are synchronous or asynchronous

    void Execute(Action method, int tryTimes, int interval)
    TResult Execute<TResult>(Func<TResult> function, int tryTimes, int interval)
    Task ExecuteAsync<TResult>(Func<Task> asyncMethod, int tryTimes, int interval)
    Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> asyncFunction, int tryTimes, int interval)