Open sandboxFocusImprove this doc

Retry example, step 1: Getting started

In this first example, we demonstrate a simple aspect that catches all exceptions and retries the method execution until the number of attempts reaches a maximum.

Let's see what this aspect does with the following code:

Source Code



1internal static class RemoteCalculator
2{
3    private static int _attempts;
4
5    [Retry( Attempts = 5 )]
6    public static int Add( int a, int b )
7    {




8        // Let's pretend this method executes remotely
9        // and can fail for network reasons.
10
11        Thread.Sleep( 10 );
12
13        _attempts++;
14        Console.WriteLine( $"Trying for the {_attempts}-th time." );
15
16        if ( _attempts <= 3 )
17        {
18            throw new InvalidOperationException();
19        }
20
21        Console.WriteLine( $"Succeeded." );
22
23        return a + b;








24    }
25
26    [Retry( Attempts = 5 )]
27    public static async Task<int> AddAsync( int a, int b )
28    {
















29        // Let's pretend this method executes remotely
30        // and can fail for network reasons.
31
32        await Task.Delay( 10 );
33
34        _attempts++;
35        Console.WriteLine( $"Trying for the {_attempts}-th time." );
36
37        if ( _attempts <= 3 )
38        {
39            throw new InvalidOperationException();
40        }
41
42        Console.WriteLine( $"Succeeded." );
43
44        return a + b;
45    }
46}
Transformed Code
1using System;
2using System.Threading;
3
4internal static class RemoteCalculator
5{
6    private static int _attempts;
7
8    [Retry( Attempts = 5 )]
9    public static int Add( int a, int b )
10    {
11for (var i = 0; ; i++)
12        {
13            try
14            {
15                // Let's pretend this method executes remotely
16                // and can fail for network reasons.
17
18                Thread.Sleep( 10 );
19
20        _attempts++;
21        Console.WriteLine( $"Trying for the {_attempts}-th time." );
22
23        if ( _attempts <= 3 )
24        {
25            throw new InvalidOperationException();
26        }
27
28        Console.WriteLine( $"Succeeded." );
29
30        return a + b;
31}
32            catch (Exception e) when (i < 5)
33            {
34                var delay = 100 * Math.Pow(2, i + 1);
35                Console.WriteLine(e.Message + $" Waiting {delay} ms.");
36                Thread.Sleep((int)delay);
37            }
38        }
39    }
40
41    [Retry( Attempts = 5 )]
42    public static async Task<int> AddAsync( int a, int b )
43    {
44for (var i = 0; ; i++)
45        {
46            try
47            {
48                return (await RemoteCalculator.AddAsync_Source(a, b));
49            }
50            catch (Exception e) when (i < 5)
51            {
52                var delay = 100 * Math.Pow(2, i + 1);
53                Console.WriteLine(e.Message + $" Waiting {delay} ms.");
54                Thread.Sleep((int)delay);
55            }
56        }
57    }
58private static async Task<int> AddAsync_Source(int a, int b)
59    {
60        // Let's pretend this method executes remotely
61        // and can fail for network reasons.
62
63        await Task.Delay( 10 );
64
65        _attempts++;
66        Console.WriteLine( $"Trying for the {_attempts}-th time." );
67
68        if ( _attempts <= 3 )
69        {
70            throw new InvalidOperationException();
71        }
72
73        Console.WriteLine( $"Succeeded." );
74
75        return a + b;
76    }
77}

When we call this method, it produces the following output:

Trying for the 1-th time.
Operation is not valid due to the current state of the object. Waiting 200 ms.
Trying for the 2-th time.
Operation is not valid due to the current state of the object. Waiting 400 ms.
Trying for the 3-th time.
Operation is not valid due to the current state of the object. Waiting 800 ms.
Trying for the 4-th time.
Succeeded

Implementation

The aspect is implemented by the RetryAttribute class.

1using Metalama.Framework.Aspects;
2
3public class RetryAttribute : OverrideMethodAspect
4{
5    /// <summary>
6    /// Gets or sets the maximum number of times that the method should be executed.
7    /// </summary>
8    public int Attempts { get; set; } = 3;
9
10    /// <summary>
11    /// Gets or set the delay, in ms, to wait between the first and the second attempt.
12    /// The delay is doubled at every further attempt.
13    /// </summary>
14    public double Delay { get; set; } = 100;
15
16    public override dynamic? OverrideMethod()
17    {
18        for ( var i = 0;; i++ )
19        {
20            try
21            {
22                return meta.Proceed();
23            }
24            catch ( Exception e ) when ( i < this.Attempts )
25            {
26                var delay = this.Delay * Math.Pow( 2, i + 1 );
27                Console.WriteLine( e.Message + $" Waiting {delay} ms." );
28                Thread.Sleep( (int) delay );
29            }
30        }
31    }
32}

The RetryAttribute class derives from the OverrideMethodAspect abstract class, which in turn derives from the System.Attribute class. This makes RetryAttribute a custom attribute.

The RetryAttribute class implements the OverrideMethod method. This method acts like a template. Most of the code in this template is injected into the target method, i.e., the method to which we add the [Retry] custom attribute.

Inside the OverrideMethod implementation, the call to meta.Proceed() has a special meaning. When the aspect is applied to the target, the call to meta.Proceed() is replaced by the original implementation, but with a few syntactic changes to capture the return value. We colored meta.Proceed() differently than the other code to remind you that it has a special meaning. If you use Metalama Tools for Visual Studio, you will also enjoy syntax highlighting in this IDE.

To implement the retry behavior, we wrap the call to meta.Proceed() within a for loop and try...catch exception handler.

The RetryAttribute class has two properties: Delay and Attempts. The value of these properties is used in the OverrideMethod implementation. Because the value of these properties is known and compile time, it will replace them with their value in the template.

Limitations

This first example has severe limitations:

  • The async variant should use Task.Delay instead of Thread.Sleep.
  • The logging is too basic and hardcoded to Console.WriteLine.

We will address these limitations in the following examples.