Open sandboxFocusImprove this doc

Retry example, step 2: Handling async methods

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.

Source Code
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}
Transformed Code
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.