1

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?

SeReGa
  • 1,219
  • 2
  • 11
  • 32
  • Does this answer your question? [Does C# support a variable number of arguments, and how?](https://stackoverflow.com/questions/9528276/does-c-sharp-support-a-variable-number-of-arguments-and-how) – Heretic Monkey Mar 22 '23 at 19:17
  • 4
    Isn't that reinventing the wheel, when we have [Polly](https://github.com/App-vNext/Polly)? – Fildor Mar 22 '23 at 19:20
  • @HereticMonkey, Thanks, but it's not really the same. When I pass the function, I need to pass the types of parameters as well. Which means, if I pass the parameters as `params`, the function will "not know" that it should get parameters... – SeReGa Mar 22 '23 at 19:26
  • Thanks @Fildor, if don't find an easy solution I will probably use it. I still think it doesn't worth importing a whole package for this simple task. – SeReGa Mar 22 '23 at 19:33
  • 1
    Since the retry logic simply forwards the arguments, why not stick to a single method that just takes a `Func`? Consumers will just pass a closure in which all the arguments are filled in. The function's arguments are opaque to the retry method in all of your examples anyway. – Aluan Haddad Mar 23 '23 at 05:48
  • You can already handle arbitrary parameters using partial application – Panagiotis Kanavos Mar 23 '23 at 08:41
  • You only need the multi-parameter overloads for performance reasons, to avoid allocating a new delegate on each call. – Panagiotis Kanavos Mar 23 '23 at 08:50

5 Answers5

4

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)
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
2

I made a nuget package called Mulligan for accomplishing this; if you don't want to use my package, you could at least see an example of doing retries. I created this package after wanting to use the retry code found in TestStack.White outside that library without copying and pasting it each time. The core functionality in Mulligan can be found in Retry.cs.

You could also look at how Polly implements their retry logic if you really need to reimplement it yourself and it is not .NET code, but this library is a thorough implementation as well. Whenever I get the time, I plan to look at these libraries and port as much as I can to mulligan, so you might find this a useful source of inspiration.

Hopefully, seeing all these side-by-side gives you some good examples to work off of, as well as the progression between implementations at different maturity levels.

Max Young
  • 1,522
  • 1
  • 16
  • 42
2

To be truly generic, your function would have to have all sorts of prototypes to handle different numbers of arguments, out, ref, async, etc. What a pain! Fortunately it doesn't have to handle the arguments at all. Instead of wrapping a function, wrap the invocation of that function, including assignment of parameters.

public static void Execute(Action action, int tryTimes, int interval)
{
    for (int i = 0; i < tryTimes - 1; i++)
    {
        try
        {
            action();
        }
        catch (Exception)
        {
            Thread.Sleep(interval);
        }
    }

    action();
}

Then you just invoke it like this:

Execute( () => {
    Foo();  //Don't pass any arguments or read any result
}, 1, 2000);

Or

Execute( () => {
    var x = Foo(2);  //One numeric input and read the return value
}, 3, 1000);

Or

Execute( () => {
    Foo("Hello", out var x);  //Use one string argument and read the out parameter
}, 5, 2000);

If you want to handle async too, add one more prototype so the caller can access the task (and so you can use Task.Delay instead of Sleep):

public static async Task Execute(Func<Task> action, int tryTimes, int interval)
{
    for (int i = 0; i < tryTimes - 1; i++)
    {
        try
        {
            return await action();
        }
        catch (Exception)
        {
            await Task.Delay(interval);
        }
    }

    return await action();
}

and use like this:

await Execute( async () => {
    var x = await Foo();
}, 5, 2000);
John Wu
  • 50,556
  • 8
  • 44
  • 80
1

You can already handle arbitrary parameters using partial application.

Retrier.Execute(()=>OtherFunc(1,"banana",DateTime.Now),tryTimes:3,interval:5000);

This is a common technique in functional programming and LINQ - a Where operation expects a Func<T,bool> which usually involves variables and members outside the T type, eg :

var cutoff=DateTime.Today.AddDays(-5);

var newUsers=users.Where(user=>user.Created>cutoff);

This allocates a new delegate each time it's called, which is why some libraries have overloads for multiple parameters, purely for performance reasons.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
1

So taking into account the answers so far, and my requirements from this task I solved this by implementing 2 methods (One for methods that return a value (Func) and one for methods that do not return a value (Action).

public class Retrier
{
    /// <summary>
    /// Execute a method with a generic return value
    /// Try 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 that does not return a value
    /// Try multiple times with an interval
    /// </summary>
    /// <param name="action"></param>
    /// <param name="tryTimes"></param>
    /// <param name="interval"></param>
    public static void Execute(Action action, int tryTimes, int interval)
    {
        for (int i = 0; i < tryTimes - 1; i++)
        {
            try
            {
                action();
            }
            catch (Exception)
            {
                Thread.Sleep(interval);
            }
        }

        action();
    }
}

Now, when I need it to return a value, I can call it this way:

var result = Retrier.Execute(() =>
{
    return _client.GetSomthing(param1, param2, param3);
}, _maxCallTries, _callRetriesIntervalMilliseconds);

And when I need it to run somthing, I can use this:

Retrier.Execute(() =>
{
    _client.DoSomthing(param1, param2, param3);
}, _maxCallTries, _callRetriesIntervalMilliseconds);
SeReGa
  • 1,219
  • 2
  • 11
  • 32