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 {for (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 [Retry( Attempts = 5 )]
41 public async Task<int> AddAsync( int a, int b )
42 {for (var i = 0; ; i++)
43 {
44 try
45 {
46 return (int)(await this.AddAsync_Source(a, b));
47 }
48 catch (Exception e) when (i < 5)
49 {
50 var delay = 100 * Math.Pow(2, i + 1);
51 Console.WriteLine(e.Message + $" Waiting {delay} ms.");
52 await Task.Delay((int)delay);
53 }
54 }
55 }
56 private async Task<int> AddAsync_Source(int a, int b)
57 {
58 // Let's pretend this method executes remotely
59 // and can fail for network reasons.
60
61 await Task.Delay( 10 );
62
63 _attempts++;
64 Console.WriteLine( $"Trying for the {_attempts}-th time." );
65
66 if ( _attempts <= 3 )
67 {
68 throw new InvalidOperationException();
69 }
70
71 Console.WriteLine( $"Succeeded." );
72
73 return a + b;
74 }
75}
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.