In the previous example, async methods were handled using the same template as that of the normal methods.
Consequently, we used a synchronous call to Thread.Sleep instead of an asynchronous await Task.Delay, which
essentially negated the async nature of the original method.
This new aspect addresses this problem, providing a template that is meant specifically for async methods.
1internal class RemoteCalculator
2{
3 private static int _attempts;
4
5 [Retry( Attempts = 5 )]
6 public 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 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}
1internal class RemoteCalculator
2{
3 private static int _attempts;
4
5 [Retry( Attempts = 5 )]
6 public int Add( int a, int b )
7 {for (var i = 0; ; i++)
8 {
9 try
10 {
11 // Let's pretend this method executes remotely
12 // and can fail for network reasons.
13
14 Thread.Sleep( 10 );
15
16 _attempts++;
17 Console.WriteLine( $"Trying for the {_attempts}-th time." );
18
19 if ( _attempts <= 3 )
20 {
21 throw new InvalidOperationException();
22 }
23
24 Console.WriteLine( $"Succeeded." );
25
26 return a + b;
27}
28 catch (Exception e) when (i < 5)
29 {
30 var delay = 100 * Math.Pow(2, i + 1);
31 Console.WriteLine(e.Message + $" Waiting {delay} ms.");
32 Thread.Sleep((int)delay);
33 }
34 }
35 }
36 [Retry( Attempts = 5 )]
37 public async Task<int> AddAsync( int a, int b )
38 {for (var i = 0; ; i++)
39 {
40 try
41 {
42 return (int)(await this.AddAsync_Source(a, b));
43 }
44 catch (Exception e) when (i < 5)
45 {
46 var delay = 100 * Math.Pow(2, i + 1);
47 Console.WriteLine(e.Message + $" Waiting {delay} ms.");
48 await Task.Delay((int)delay);
49 }
50 }
51 }
52 private async Task<int> AddAsync_Source(int a, int b)
53 {
54 // Let's pretend this method executes remotely
55 // and can fail for network reasons.
56
57 await Task.Delay( 10 );
58
59 _attempts++;
60 Console.WriteLine( $"Trying for the {_attempts}-th time." );
61
62 if ( _attempts <= 3 )
63 {
64 throw new InvalidOperationException();
65 }
66
67 Console.WriteLine( $"Succeeded." );
68
69 return a + b;
70 }
71}
Implementation
The aspect provides a second template, OverrideAsyncMethod(), which
will provide the async implementation of the method.
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 // Template for non-async methods.
17 public override dynamic? OverrideMethod()
18 {
19 for ( var i = 0;; i++ )
20 {
21 try
22 {
23 return meta.Proceed();
24 }
25 catch ( Exception e ) when ( i < this.Attempts )
26 {
27 var delay = this.Delay * Math.Pow( 2, i + 1 );
28 Console.WriteLine( e.Message + $" Waiting {delay} ms." );
29 Thread.Sleep( (int) delay );
30 }
31 }
32 }
33
34 // Template for async methods.
35 public override async Task<dynamic?> OverrideAsyncMethod()
36 {
37 for ( var i = 0;; i++ )
38 {
39 try
40 {
41 return await meta.ProceedAsync();
42 }
43 catch ( Exception e ) when ( i < this.Attempts )
44 {
45 var delay = this.Delay * Math.Pow( 2, i + 1 );
46 Console.WriteLine( e.Message + $" Waiting {delay} ms." );
47
48 await Task.Delay( (int) delay );
49 }
50 }
51 }
52}
The async template uses await meta.ProceedAsync() instead of meta.Proceed(), and await Task.Delay instead
of Thread.Sleep.
Limitations
There are still two limitations in this example:
- The aspect does not correctly handle a
CancellationToken. - The logging is very basic and is hardcoded to
Console.WriteLine.