MetalamaCommented examplesException HandlingRetryStep 5.​ Using Polly
Open sandboxFocusImprove this doc

Retry example, step 5: Using Polly

If you had the feeling that we were reinventing the wheel in the previous Retry examples, you were partially right. Libraries like Polly offer more advanced and configurable retry policies, but even Polly requires some boilerplate code. Wrapping the whole method body in a delegate call and adding proper logging with parameter values entails boilerplate. With a Metalama aspect, we can completely avoid this boilerplate.

Infrastructure code

Before jumping into the implementation, let's consider the architecture and infrastructure code.

We want the Polly policy to be centrally configurable. In a production environment, part of this configuration may be read from an XML or JSON file. The user of this aspect will only need to specify which kind of policy is required for the target method by specifying a PolicyKind, a new enum we will define. Then the target method will obtain the policy from our a IPolicyFactory, which will map the PolicyKind to a Polly Policy object. The PolicyFactory implementation is supplied by the dependency injection framework. Its implementation would typically differ in a testing or production environment.

PolicyKind

This enum represents the kinds of policies that are available to business code. You can extend it at will.

1using Metalama.Framework.Aspects;
2
3[RunTimeOrCompileTime]
4public enum PolicyKind
5{
6    Retry
7}

We can imagine having several aspect custom attributes for each PolicyKind. You can add parameters to policies, as long as these parameters can be implemented as properties of a custom attribute.

IPolicyFactory

This interface is responsible for returning an instance of the Policy class that corresponds to the given PolicyKind.

1using Polly;
2
3public interface IPolicyFactory
4{
5    Policy GetPolicy( PolicyKind policyKind );
6    AsyncPolicy GetAsyncPolicy( PolicyKind policyKind );
7}

PolicyFactory

Here is a minimalistic implementation of the IPolicyFactory class. You can make it as complex as necessary, but this goes beyond the scope of this example. This class must be added to the dependency collection.

1using Polly;
2using Polly.Retry;
3
4internal class PolicyFactory : IPolicyFactory
5{
6    private static readonly RetryPolicy _retry = Policy.Handle<Exception>().WaitAndRetry(
7        new[]
8        {
9            TimeSpan.FromSeconds( 1 ), TimeSpan.FromSeconds( 2 ), TimeSpan.FromSeconds( 4 ),
10            TimeSpan.FromSeconds( 8 ), TimeSpan.FromSeconds( 15 ), TimeSpan.FromSeconds( 30 )
11        } );
12
13    private static readonly AsyncRetryPolicy _asyncRetry = Policy.Handle<Exception>().WaitAndRetryAsync(
14        new[]
15        {
16            TimeSpan.FromSeconds( 1 ), TimeSpan.FromSeconds( 2 ), TimeSpan.FromSeconds( 4 ),
17            TimeSpan.FromSeconds( 8 ), TimeSpan.FromSeconds( 15 ), TimeSpan.FromSeconds( 30 )
18        } );
19
20
21    public Policy GetPolicy( PolicyKind policyKind )
22        => policyKind switch
23        {
24            PolicyKind.Retry => _retry,
25            _ => throw new ArgumentOutOfRangeException( nameof(policyKind) )
26        };
27
28    public AsyncPolicy GetAsyncPolicy( PolicyKind policyKind )
29        => policyKind switch
30        {
31            PolicyKind.Retry => _asyncRetry,
32            _ => throw new ArgumentOutOfRangeException( nameof(policyKind) )
33        };
34}

Business code

Let's compare the source business code and the transformed business code.

Source Code





1internal class RemoteCalculator
2{
3    private static int _attempts;
4
5    [Retry]
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]
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]
11    public int Add( int a, int b )
12    {
13object? ExecuteCore()
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)
35            {
36                LoggerExtensions.LogWarning(this._logger, $"RemoteCalculator.Add(a = {{{a}}}, b = {{{b}}}) has failed: {e.Message}");
37                throw;
38            }
39        }
40
41        var policy = this._policyFactory.GetPolicy(PolicyKind.Retry);
42        return (int)policy.Execute(ExecuteCore);
43    }
44
45    [Retry]
46    public async Task<int> AddAsync( int a, int b )
47    {
48async Task<object?> ExecuteCoreAsync(CancellationToken cancellationToken) { try { return await this.AddAsync_Source(a, b); } catch (Exception e) { LoggerExtensions.LogWarning(this._logger, $"RemoteCalculator.AddAsync(a = {{{a}}}, b = {{{b}}}) has failed: {e.Message}"); throw; } }
49
50        var policy = this._policyFactory.GetAsyncPolicy(PolicyKind.Retry);
51        return (int)await policy.ExecuteAsync(ExecuteCoreAsync, CancellationToken.None);
52    }
53private async Task<int> AddAsync_Source(int a, int b)
54    {
55        // Let's pretend this method executes remotely
56        // and can fail for network reasons.
57
58        await Task.Delay( 10 );
59
60        _attempts++;
61        Console.WriteLine( $"Trying for the {_attempts}-th time." );
62
63        if ( _attempts <= 3 )
64        {
65            throw new InvalidOperationException();
66        }
67
68        Console.WriteLine( $"Succeeded." );
69
70        return a + b;
71    }
72    private ILogger _logger;
73    private IPolicyFactory _policyFactory;
74
75    public RemoteCalculator
76    (ILogger<RemoteCalculator> logger = default(global::Microsoft.Extensions.Logging.ILogger<global::RemoteCalculator>), IPolicyFactory? policyFactory = default(global::IPolicyFactory?))
77    {
78        this._logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); this._policyFactory = policyFactory ?? throw new System.ArgumentNullException(nameof(policyFactory));
79    }
80}

Aspect implementation

Here is the source code of the Retry aspect:

1using Metalama.Extensions.DependencyInjection;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Code.SyntaxBuilders;
5using Microsoft.Extensions.Logging;
6
7#pragma warning disable CS8618, CS0649
8
9public class RetryAttribute : OverrideMethodAspect
10{
11    [IntroduceDependency]
12    private readonly ILogger _logger;
13
14    [IntroduceDependency]
15    private readonly IPolicyFactory _policyFactory;
16
17    public PolicyKind Kind { get; }
18
19    public RetryAttribute( PolicyKind kind = PolicyKind.Retry )
20    {
21        this.Kind = kind;
22    }
23
24    // Template for non-async methods.
25    public override dynamic? OverrideMethod()
26    {
27        object? ExecuteCore()
28        {
29            try
30            {
31                return meta.Proceed();
32            }
33            catch ( Exception e )
34            {
35                var messageBuilder = LoggingHelper.BuildInterpolatedString( false );
36                messageBuilder.AddText( " has failed: " );
37                messageBuilder.AddExpression( e.Message );
38                this._logger.LogWarning( (string) messageBuilder.ToValue() );
39
40                throw;
41            }
42        }
43
44        var policy = this._policyFactory.GetPolicy( this.Kind );
45        return policy.Execute( ExecuteCore );
46    }
47
48    // Template for async methods.
49    public override async Task<dynamic?> OverrideAsyncMethod()
50    {
51        async Task<object?> ExecuteCoreAsync( CancellationToken cancellationToken )
52        {
53            try
54            {
55                return await meta.ProceedAsync();
56            }
57            catch ( Exception e )
58            {
59                var messageBuilder = LoggingHelper.BuildInterpolatedString( false );
60                messageBuilder.AddText( " has failed: " );
61                messageBuilder.AddExpression( e.Message );
62                this._logger.LogWarning( (string) messageBuilder.ToValue() );
63
64                throw;
65            }
66        }
67
68        var cancellationTokenParameter
69            = meta.Target.Parameters.LastOrDefault( p => p.Type.Is( typeof(CancellationToken) ) );
70
71        var policy = this._policyFactory.GetAsyncPolicy( this.Kind );
72        return await policy.ExecuteAsync( ExecuteCoreAsync,
73            cancellationTokenParameter != null
74                ? (CancellationToken) cancellationTokenParameter.Value!
75                : CancellationToken.None );
76    }
77}

This aspect pulls in two dependencies, ILogger and IPolicyFactory. The [IntroduceDependency] custom attribute on top of the fields introduces the fields and pulls them from the constructor.

Polly, by design, requires the logic to be wrapped in a delegate. In a Metalama template, we achieve this only with a local function because anonymous methods or lambdas can't contain calls to meta.Proceed. These local functions are named ExecuteCore or ExecuteCoreAsync. They have an exception handler that prints a warning with the method name and all method arguments when the method fails.

We now have the best of both worlds: a fully-featured resilience framework with Polly, and boilerplate reduction with Metalama.