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:
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}
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 useTask.Delay
instead ofThread.Sleep
. - The logging is too basic and hardcoded to
Console.WriteLine
.
We will address these limitations in the following examples.