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    {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.