MetalamaCommented examplesException HandlingRetryStep 2.​ Handling async methods
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
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.