In steps 1 to 5, the [Retry] aspect overrides the method to which it is applied. This requires the source code of
the method. However, the calls that fail for transient reasons are often calls to a library that you do not own, for
instance the client of a remote service. You cannot add an aspect to the methods of this library.
In this example, the application calls ExchangeRateClient, a class of a separate class library. The library does not
use Metalama. To simulate transient failures, each method of ExchangeRateClient fails the first two times it is
called.
Instead of overriding the methods of ExchangeRateClient, Metalama rewrites the calls to these methods in the source
code of the application. Each call calls a generated interceptor method, which retries the call. The calling code
itself does not change in the source code:
1using Metalama.Samples.Retry.ExchangeRates;
2
3namespace Metalama.Samples.Retry6;
4
5public class PriceService
6{
7 private readonly ExchangeRateClient _client = new();
8
9 public decimal ConvertPrice( decimal price, string currency ) => price * this._client.GetRate( currency );
10
11 public async Task<decimal> ConvertPriceAsync( decimal price, string currency, CancellationToken cancellationToken )
12 => price * await this._client.GetRateAsync( currency, cancellationToken );
13
14 public Task<string> BuyCurrencyAsync( string currency, decimal amount, CancellationToken cancellationToken )
15 => this._client.BuyCurrencyAsync( currency, amount, cancellationToken );
16}
17
1using Metalama.Samples.Retry.ExchangeRates;
2
3namespace Metalama.Samples.Retry6;
4
5public class PriceService
6{
7 private readonly ExchangeRateClient _client = new();
8
9 public decimal ConvertPrice( decimal price, string currency ) => price * MetalamaInterceptors.ExchangeRateClient_GetRate_Interceptor( this._client,currency:currency );
10
11 public async Task<decimal> ConvertPriceAsync( decimal price, string currency, CancellationToken cancellationToken )
12 => price * await MetalamaInterceptors.ExchangeRateClient_GetRateAsync_Interceptor( this._client,currency:currency,cancellationToken:cancellationToken );
13
14 public Task<string> BuyCurrencyAsync( string currency, decimal amount, CancellationToken cancellationToken )
15 => this._client.BuyCurrencyAsync( currency, amount, cancellationToken );
16}
17
When the program runs, it produces the following output:
The exchange rate service did not respond in time. Waiting 200 ms.
The exchange rate service did not respond in time. Waiting 400 ms.
100 USD = 92.00 EUR
The exchange rate service did not respond in time. Waiting 200 ms.
The exchange rate service did not respond in time. Waiting 400 ms.
100 USD = 79.00 GBP
Order 1: 50 EUR
Implementation
The project references the Metalama.Extensions.Interceptors package. The retry logic is implemented by the following
project fabric:
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Advising;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Fabrics;
5using Metalama.Samples.Retry.ExchangeRates;
6
7namespace Metalama.Samples.Retry6;
8
9// Retries the calls to the idempotent methods of ExchangeRateClient, a class of a library that you do not own.
10internal class RetryFabric : ProjectFabric
11{
12 /// <summary>
13 /// Gets the maximum number of times that the call should be retried.
14 /// </summary>
15 private int Attempts => 3;
16
17 /// <summary>
18 /// Gets the delay, in ms, to wait before the first retry.
19 /// The delay is doubled at every further retry.
20 /// </summary>
21 private double Delay => 100;
22
23 public override void AmendProject( IProjectAmender amender )
24 => amender.InterceptMethods(
25 calls => calls
26 .Type( typeof(ExchangeRateClient) )
27 .AllMethods()
28
29 // Retry only the methods that read data. Retrying BuyCurrencyAsync could place the order twice.
30 .Where( m => m.Name.StartsWith( "Get", StringComparison.Ordinal ) )
31 .RedirectToSynthesizedMethod(
32 new MethodTemplateSelector( nameof(this.Retry), nameof(this.RetryAsync) ) ) );
33
34 // Template for non-async methods.
35 [Template]
36 public dynamic? Retry()
37 {
38 for ( var i = 0;; i++ )
39 {
40 try
41 {
42 return meta.Proceed();
43 }
44 catch ( Exception e ) when ( i < this.Attempts )
45 {
46 var delay = this.Delay * Math.Pow( 2, i + 1 );
47 Console.WriteLine( e.Message + $" Waiting {delay} ms." );
48 Thread.Sleep( (int) delay );
49 }
50 }
51 }
52
53 // Template for async methods. The cancellationToken parameter binds by name to the parameter of the
54 // intercepted method.
55 [Template]
56 public async Task<dynamic?> RetryAsync( CancellationToken cancellationToken = default )
57 {
58 for ( var i = 0;; i++ )
59 {
60 try
61 {
62 return await meta.ProceedAsync();
63 }
64 catch ( Exception e ) when ( i < this.Attempts )
65 {
66 var delay = this.Delay * Math.Pow( 2, i + 1 );
67 Console.WriteLine( e.Message + $" Waiting {delay} ms." );
68 await Task.Delay( (int) delay, cancellationToken );
69 }
70 }
71 }
72}
73
The AmendProject method registers the interceptors with the InterceptMethods method:
Type( typeof(ExchangeRateClient) ).AllMethods()selects all the methods thatExchangeRateClientdeclares. Metalama intercepts the calls to these methods in the source code of the current project.Wherekeeps only the methods whose name starts withGet. These methods read data, so calling them twice has the same effect as calling them once. In other words, they are idempotent.BuyCurrencyAsyncis not idempotent: if the first call places the order but fails before it returns, a retry places a second order. Therefore, the calls toBuyCurrencyAsyncare not intercepted.RedirectToSynthesizedMethodgenerates the interceptor methods from the templates of the fabric. TheMethodTemplateSelectorgives two templates:Retryfor the methods that are not async, andRetryAsyncfor the async methods.
The Retry and RetryAsync templates have the same code as the templates of the [Retry] aspect of step 3, with the
following differences:
meta.Proceed()andmeta.ProceedAsync()call the intercepted method ofExchangeRateClient, with the arguments of the call site.RetryAsyncdeclares a run-time parametercancellationToken, and passes it toTask.Delay. Metalama binds the run-time parameters of the template to the parameters of the interceptor by name, socancellationTokenreceives theCancellationTokenargument of the call. For details, see Interceptor binding rules.- The maximum number of attempts and the delay are properties of the fabric, instead of properties of the aspect.
Metalama generates the interceptor methods in an internal static class named MetalamaInterceptors, in the global
namespace. The interceptor of GetRateAsync is an async method, and it awaits the call to GetRateAsync inside the
retry loop.
Comparison with steps 1 to 5
| Steps 1 to 5: aspect | Step 6: interceptors | |
|---|---|---|
| What changes | The implementation of the method. | The calls to the method, in the current project. |
| Source code of the method | Required. | Not required. The method can be in any assembly. |
| Selection of the methods | You add the [Retry] attribute to each method. |
A fabric selects the methods with a query, for instance by name. |
| Callers that retry | All the callers of the method. | Only the callers in the projects that register the interceptors. |
Use the aspect of steps 1 to 5 when you own the method and all its callers must retry it. Use interceptors when you do not own the method, or when only some callers must retry it.