Metalama//Commented Examples/Exception Handling/Retry/Step 2.​ Handling async methods
Open sandboxFocus

Retry example, step 2: Handling async methods

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

Implementation

The aspect provides a second template, OverrideAsyncMethod(), which provides the async implementation of the method.

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3using Microsoft.Extensions.Logging;
4
5public class RetryAttribute : OverrideMethodAspect
6{
7    [IntroduceDependency]
8    private readonly ILogger _logger;
9
10    /// <summary>
11    /// Gets or sets the maximum number of times that the method should be executed.
12    /// </summary>
13    public int Attempts { get; set; } = 3;
14
15    /// <summary>
16    /// Gets or set the delay, in ms, to wait between the first and the second attempt.
17    /// The delay is doubled at every further attempt.
18    /// </summary>
19    public double Delay { get; set; } = 100;
20
21    // Template for non-async methods.
22    public override dynamic? OverrideMethod()
23    {
24        for ( var i = 0;; i++ )
25        {
26            try
27            {
28                return meta.Proceed();
29            }
30            catch ( Exception e ) when ( i < this.Attempts )
31            {
32                var delay = this.Delay * Math.Pow( 2, i + 1 );
33                Console.WriteLine( e.Message + $" Waiting {delay} ms." );
34                Thread.Sleep( (int) delay );
35            }
36        }
37    }
38
39    // Template for async methods.
40    public override async Task<dynamic?> OverrideAsyncMethod()
41    {
42        for ( var i = 0;; i++ )
43        {
44            try
45            {
46                return await meta.ProceedAsync();
47            }
48            catch ( Exception e ) when ( i < this.Attempts )
49            {
50                var delay = this.Delay * Math.Pow( 2, i + 1 );
51                Console.WriteLine( e.Message + $" Waiting {delay} ms." );
52
53                await Task.Delay( (int) delay );
54            }
55        }
56    }
57}

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.