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