When you intercept many methods with different signatures, writing one interceptor per signature is not practical. Instead, write a T# template once, and let Metalama generate an interceptor for each signature.
This article assumes that you know how to write T# templates. See Writing T# templates.
Writing an interceptor template
To generate interceptors from a template:
- Write a method with the
[Template]attribute. Its return type isdynamic?, so that the template can implement methods of any return type. - In the template, call
meta.Proceed()to call the intercepted method with the arguments of the call site. - In the description of the interception, call RedirectToSynthesizedMethod with the name of the template.
Interceptor templates look like the templates of override aspects, with two differences:
meta.Target.Methodis the generated interceptor, not the intercepted method.meta.Proceed()calls the intercepted method, as the call site did.
For instance, the following template measures the duration of the intercepted call:
20public dynamic? Time()
21{
22 var stopwatch = Stopwatch.StartNew();
23
24 try
25 {
26 return meta.Proceed();
27 }
28 finally
29 {
30 var methodName = meta.Interception.Method.InterceptedMethod.Name;
31 Console.WriteLine( $"{methodName} took {stopwatch.ElapsedMilliseconds} ms." );
32 }
33}By default, the template is a member of the aspect or of the fabric that registers the interception. To declare the templates in another class, implement ITemplateProvider in this class, and give an instance of it with WithTemplateProvider.
For instance, the following fabric registers the interception. Its Trace method is the template:
13public override void AmendProject( IProjectAmender amender )
14 => amender.InterceptMethods(
15 calls => calls
16 .Type( typeof(PricingClient) )
17 .AllMethods()
18 .RedirectToSynthesizedMethod( nameof(this.Trace) )
19 .WithGranularity( InterceptorGranularity.Member ) );Accessing the interception context
The meta.Interception property describes the interception that the template implements. Its Method property gives:
- InterceptedMethod: the method that the call site calls.
- ReceiverParameter: the parameter of the generated method that receives the receiver of the call, or
nullwhen the intercepted method is static. - ArgumentParameters: the parameters of the generated method that receive the arguments of the call.
- AddedParameters: the parameters that you add, described below.
meta.Interception is a C# 14 extension property. In a project that uses an earlier version of C#, use meta.GetExtension<IInterceptionInfo>().
For instance, the following template code lists the names and values of the arguments, and the name of the intercepted method:
26var arguments = new List<string>();
27
28foreach ( var parameter in meta.Interception.Method.ArgumentParameters )
29{
30 arguments.Add( $"{parameter.Name}: {parameter.Value}" );
31}
32
33var callerName = meta.Interception.CallingMember.Name;
34var methodName = meta.Interception.Method.InterceptedMethod.Name;
35var call = $"{callerName} -> {methodName}({string.Join( ", ", arguments )})";Reading the calling code
By default, a generated interceptor is shared by all the call sites that need the same code, even when they are in different types. The template therefore cannot read information about one call site, such as the calling member. To read it, declare which information the template depends on with WithGranularity:
- With
InterceptorGranularity.Type, the template can readmeta.Interception.CallingType. - With
InterceptorGranularity.Member, it can also readmeta.Interception.CallingMember. - With
InterceptorGranularity.CallSite, it can also readmeta.Interception.Callerandmeta.Interception.Source.
A finer granularity generates more methods. To pass information about the call site without generating more methods, pass it as an added parameter instead. See Interceptor deduplication rules.
For instance, the following registration sets the Member granularity:
17.WithGranularity( InterceptorGranularity.Member ) );The template then reads the calling member:
25Console.WriteLine(
26 $"{meta.Interception.CallingMember.Name} calls "
27 + $"{meta.Interception.Method.InterceptedMethod.Name}." );Example: Timing every call to an SDK
The following project fabric measures the duration of every call to the methods of PaymentGateway, a class of another project. AllMethods selects all the methods that the type declares. The interceptor of the async method RefundAsync awaits the call, so the duration covers the whole asynchronous operation.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Fabrics;
4using System;
5using System.Diagnostics;
6
7namespace Doc.TimeSdkCalls;
8
9internal class Fabric : ProjectFabric
10{
11 public override void AmendProject( IProjectAmender amender )
12 => amender.InterceptMethods(
13 calls => calls
14 .Type( typeof(PaymentGateway) )
15 .AllMethods()
16 .RedirectToSynthesizedMethod( nameof(this.Time) ) );
17
18 [Template]
20 public dynamic? Time()
21 {
22 var stopwatch = Stopwatch.StartNew();
23
24 try
25 {
26 return meta.Proceed();
27 }
28 finally
29 {
30 var methodName = meta.Interception.Method.InterceptedMethod.Name;
31 Console.WriteLine( $"{methodName} took {stopwatch.ElapsedMilliseconds} ms." );
32 }
33 }
35}
36
1using System.Threading.Tasks;
2
3namespace Doc.TimeSdkCalls;
4
5public class CheckoutService
6{
7 private readonly PaymentGateway _gateway = new();
8
9 public string Pay( decimal amount ) => this._gateway.Charge( amount, "EUR" );
10
11 public Task CancelAsync( string chargeId ) => this._gateway.RefundAsync( chargeId );
12
13 public bool CanPay( decimal amount ) => this._gateway.GetBalance() >= amount;
14}
15
1using System.Threading.Tasks;
2
3namespace Doc.TimeSdkCalls;
4
5public class CheckoutService
6{
7 private readonly PaymentGateway _gateway = new();
8
9 public string Pay(decimal amount) => MetalamaInterceptors.PaymentGateway_Charge_Interceptor(this._gateway, amount: amount, currency: "EUR");
10
11 public Task CancelAsync(string chargeId) => MetalamaInterceptors.PaymentGateway_RefundAsync_Interceptor(this._gateway, chargeId: chargeId);
12
13 public bool CanPay(decimal amount) => MetalamaInterceptors.PaymentGateway_GetBalance_Interceptor(this._gateway) >= amount;
14}
15
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Doc.TimeSdkCalls;
internal static class MetalamaInterceptors
{
internal static string PaymentGateway_Charge_Interceptor(PaymentGateway receiver, decimal amount, string currency)
{
var stopwatch = Stopwatch.StartNew();
try
{
return receiver.Charge(amount, currency);
}
finally
{
Console.WriteLine($"Charge took {stopwatch.ElapsedMilliseconds} ms.");
}
}
internal static decimal PaymentGateway_GetBalance_Interceptor(PaymentGateway receiver)
{
var stopwatch = Stopwatch.StartNew();
try
{
return receiver.GetBalance();
}
finally
{
Console.WriteLine($"GetBalance took {stopwatch.ElapsedMilliseconds} ms.");
}
}
internal static async Task PaymentGateway_RefundAsync_Interceptor(PaymentGateway receiver, string chargeId)
{
var stopwatch = Stopwatch.StartNew();
try
{
await receiver.RefundAsync(chargeId);
return;
}
finally
{
Console.WriteLine($"RefundAsync took {stopwatch.ElapsedMilliseconds} ms.");
}
}
}using System.Threading.Tasks;
namespace Doc.TimeSdkCalls;
// A third-party SDK, compiled in another project.
public class PaymentGateway
{
public string Charge( decimal amount, string currency ) => $"ch_{amount}_{currency}";
public async Task RefundAsync( string chargeId ) => await Task.Yield();
public decimal GetBalance() => 1000m;
}The interceptors are generated in an internal static class named MetalamaInterceptors, which appears in its own tab of the example.
Example: Tracing calls to investigate a problem
The following fabric traces every call to PricingClient: the calling member, the arguments, the result, and the exception. It is a typical way to investigate a problem without a debugger, for instance in a CI build or when an AI agent investigates. To remove the tracing, delete the file. To enable it only in some builds, see Choosing which calls to intercept.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Fabrics;
4using System;
5using System.Collections.Generic;
6
7namespace Doc.TraceCalls;
8
9// Delete this file to remove the tracing.
10internal class TracingFabric : ProjectFabric
11{
13 public override void AmendProject( IProjectAmender amender )
14 => amender.InterceptMethods(
15 calls => calls
16 .Type( typeof(PricingClient) )
17 .AllMethods()
18 .RedirectToSynthesizedMethod( nameof(this.Trace) )
19 .WithGranularity( InterceptorGranularity.Member ) );
21
22 [Template]
23 public dynamic? Trace()
24 {
26 var arguments = new List<string>();
27
28 foreach ( var parameter in meta.Interception.Method.ArgumentParameters )
29 {
30 arguments.Add( $"{parameter.Name}: {parameter.Value}" );
31 }
32
33 var callerName = meta.Interception.CallingMember.Name;
34 var methodName = meta.Interception.Method.InterceptedMethod.Name;
35 var call = $"{callerName} -> {methodName}({string.Join( ", ", arguments )})";
37
38 try
39 {
40 var result = meta.Proceed();
41 Console.WriteLine( $"{call} returned {result}." );
42
43 return result;
44 }
45 catch ( Exception e )
46 {
47 Console.WriteLine( $"{call} threw {e.GetType().Name}: {e.Message}" );
48
49 throw;
50 }
51 }
52}
53
1namespace Doc.TraceCalls;
2
3public class Cart
4{
5 private readonly PricingClient _pricing = new();
6
7 public string Currency { get; private set; } = "EUR";
8
9 public decimal Total { get; private set; }
10
11 public void SetCountry( string country )
12 => this.Currency = this._pricing.GetCurrency( country );
13
14 public void Add( string sku, int quantity )
15 => this.Total += this._pricing.GetPrice( sku, quantity );
16}
17
1namespace Doc.TraceCalls;
2
3public class Cart
4{
5 private readonly PricingClient _pricing = new();
6
7 public string Currency { get; private set; } = "EUR";
8
9 public decimal Total { get; private set; }
10
11 public void SetCountry(string country)
12 => this.Currency = MetalamaInterceptors.PricingClient_GetCurrency_Interceptor(this._pricing, country: country);
13
14 public void Add(string sku, int quantity)
15 => this.Total += MetalamaInterceptors.PricingClient_GetPrice_Interceptor(this._pricing, sku: sku, quantity: quantity);
16}
17
using System;
using System.Collections.Generic;
using Doc.TraceCalls;
internal static class MetalamaInterceptors
{
internal static string PricingClient_GetCurrency_Interceptor(PricingClient receiver, string country)
{
var arguments = new List<string>();
arguments.Add($"country: {country}");
var call = $"SetCountry -> GetCurrency({string.Join(", ", arguments)})";
try
{
var result = receiver.GetCurrency(country);
Console.WriteLine($"{call} returned {result}.");
return result;
}
catch (Exception e)
{
Console.WriteLine($"{call} threw {e.GetType().Name}: {e.Message}");
throw;
}
}
internal static decimal PricingClient_GetPrice_Interceptor(PricingClient receiver, string sku, int quantity)
{
var arguments = new List<string>();
arguments.Add($"sku: {sku}");
arguments.Add($"quantity: {quantity}");
var call = $"Add -> GetPrice({string.Join(", ", arguments)})";
try
{
var result = receiver.GetPrice(sku, quantity);
Console.WriteLine($"{call} returned {result}.");
return result;
}
catch (Exception e)
{
Console.WriteLine($"{call} threw {e.GetType().Name}: {e.Message}");
throw;
}
}
}using System;
namespace Doc.TraceCalls;
// A third-party library, compiled in another project.
public class PricingClient
{
public decimal GetPrice( string sku, int quantity )
=> sku switch
{
"BOOK" => 12.5m * quantity,
_ => throw new ArgumentException( $"Unknown SKU '{sku}'." )
};
public string GetCurrency( string country ) => country == "US" ? "USD" : "EUR";
}1using System;
2
3namespace Doc.TraceCalls;
4
5internal class Program
6{
7 private static void Main()
8 {
9 var cart = new Cart();
10 cart.SetCountry( "US" );
11 cart.Add( "BOOK", 2 );
12
13 try
14 {
15 cart.Add( "PEN", 1 );
16 }
17 catch ( ArgumentException )
18 {
19 // The trace shows the failed call.
20 }
21 }
22}
23
SetCountry -> GetCurrency(country: US) returned USD. Add -> GetPrice(sku: BOOK, quantity: 2) returned 25.0. Add -> GetPrice(sku: PEN, quantity: 1) threw ArgumentException: Unknown SKU 'PEN'.
Changing arguments, results and exceptions
The template controls the call:
- Assign a parameter before
meta.Proceed()to change the argument that the intercepted method receives. - Store the value of
meta.Proceed(), process it, and return another value. - Surround
meta.Proceed()withtry,catchandfinally, or with a loop.
For instance, the following template retries the call when it times out:
23public dynamic? Retry( [CompileTime] int maxAttempts )
24{
25 for ( var attempt = 1;; attempt++ )
26 {
27 try
28 {
29 return meta.Proceed();
30 }
31 catch ( TimeoutException e ) when ( attempt < maxAttempts )
32 {
33 Console.WriteLine( $"Attempt {attempt} failed: {e.Message} Retrying." );
34 }
35 }
36}Async and iterator methods
When the intercepted method returns an awaitable type, such as Task, the generated interceptor is an async method, and meta.Proceed() awaits the call. The code after meta.Proceed() therefore runs after the asynchronous operation completes. This rule depends only on the return type, so it also applies when the intercepted method is not declared async. In the same way, the interceptor of an async iterator method is an async iterator.
For instance, in the timing example above, the tab of the MetalamaInterceptors class shows the interceptor of RefundAsync, which awaits the call inside the try block.
To give different templates for async or iterator methods, use a MethodTemplateSelector. The selector works as for override aspects. See Overriding methods.
Passing arguments to the template
WithArgs gives the arguments of the template, as the properties of an anonymous object:
- A property whose name is a compile-time parameter of the template gives its value. The value is known when the template is expanded.
- A property whose name is a run-time parameter of the template adds a parameter of that name to the generated method. Each call site passes the value.
For instance, the following registration gives the value of the compile-time parameter maxAttempts of the retry template:
16.Where( m => m.Name.StartsWith( "Get" ) )
17.RedirectToSynthesizedMethod( nameof(this.Retry) )
18.WithArgs( new { maxAttempts = 3 } ) );In the following template, category and caller are run-time parameters, so they become parameters of the generated method:
33public dynamic? Trace( string category, string caller )WithTags gives an object that the template reads through meta.Tags. For the complete rules, see Interceptor binding rules.
Example: Retrying calls to a third-party service
The following fabric retries the calls to a third-party shipping client when they time out. Where selects only the methods whose name starts with Get, because retrying an operation that is not idempotent, such as creating a shipment, could perform it twice. The maximum number of attempts is a compile-time parameter of the template.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Fabrics;
4using System;
5
6namespace Doc.RetryShippingCalls;
7
8internal class Fabric : ProjectFabric
9{
10 public override void AmendProject( IProjectAmender amender )
11 => amender.InterceptMethods(
12 calls => calls
13 .Type( typeof(ShippingClient) )
14 .AllMethods()
16 .Where( m => m.Name.StartsWith( "Get" ) )
17 .RedirectToSynthesizedMethod( nameof(this.Retry) )
18 .WithArgs( new { maxAttempts = 3 } ) );
20
21 [Template]
23 public dynamic? Retry( [CompileTime] int maxAttempts )
24 {
25 for ( var attempt = 1;; attempt++ )
26 {
27 try
28 {
29 return meta.Proceed();
30 }
31 catch ( TimeoutException e ) when ( attempt < maxAttempts )
32 {
33 Console.WriteLine( $"Attempt {attempt} failed: {e.Message} Retrying." );
34 }
35 }
36 }
38}
39
1using System.Threading.Tasks;
2
3namespace Doc.RetryShippingCalls;
4
5public class ShipmentService
6{
7 private readonly ShippingClient _client = new();
8
9 public decimal EstimateCost( string destination ) => this._client.GetQuote( destination );
10
11 public Task<string> GetStatusAsync( string trackingNumber )
12 => this._client.GetTrackingStatusAsync( trackingNumber );
13
14 public string Ship( string destination ) => this._client.CreateShipment( destination );
15}
16
1using System.Threading.Tasks;
2
3namespace Doc.RetryShippingCalls;
4
5public class ShipmentService
6{
7 private readonly ShippingClient _client = new();
8
9 public decimal EstimateCost(string destination) => MetalamaInterceptors.ShippingClient_GetQuote_Interceptor(this._client, destination: destination);
10
11 public Task<string> GetStatusAsync(string trackingNumber)
12 => MetalamaInterceptors.ShippingClient_GetTrackingStatusAsync_Interceptor(this._client, trackingNumber: trackingNumber);
13
14 public string Ship(string destination) => this._client.CreateShipment(destination);
15}
16
using System;
using System.Threading.Tasks;
using Doc.RetryShippingCalls;
internal static class MetalamaInterceptors
{
internal static decimal ShippingClient_GetQuote_Interceptor(ShippingClient receiver, string destination)
{
for (var attempt = 1;; attempt++)
{
try
{
return receiver.GetQuote(destination);
}
catch (TimeoutException e)when (attempt < 3)
{
Console.WriteLine($"Attempt {attempt} failed: {e.Message} Retrying.");
}
}
}
internal static async Task<string> ShippingClient_GetTrackingStatusAsync_Interceptor(ShippingClient receiver, string trackingNumber)
{
for (var attempt = 1;; attempt++)
{
try
{
return await receiver.GetTrackingStatusAsync(trackingNumber);
}
catch (TimeoutException e)when (attempt < 3)
{
Console.WriteLine($"Attempt {attempt} failed: {e.Message} Retrying.");
}
}
}
}using System;
using System.Threading.Tasks;
namespace Doc.RetryShippingCalls;
// A third-party SDK, compiled in another project. Its methods fail the first two times
// they are called.
public class ShippingClient
{
private int _failures;
public decimal GetQuote( string destination )
{
this.FailTwice();
return destination == "US" ? 25m : 12m;
}
public async Task<string> GetTrackingStatusAsync( string trackingNumber )
{
await Task.Yield();
this.FailTwice();
return "Delivered";
}
public string CreateShipment( string destination ) => $"SHP-{destination}";
private void FailTwice()
{
if ( this._failures++ % 3 < 2 )
{
throw new TimeoutException( "The shipping service did not respond." );
}
}
}1using System;
2
3namespace Doc.RetryShippingCalls;
4
5internal class Program
6{
7 private static void Main()
8 {
9 var service = new ShipmentService();
10
11 Console.WriteLine( $"Cost: {service.EstimateCost( "US" )}" );
12 Console.WriteLine(
13 $"Status: {service.GetStatusAsync( "TRK-1" ).GetAwaiter().GetResult()}" );
14 }
15}
16
Attempt 1 failed: The shipping service did not respond. Retrying. Attempt 2 failed: The shipping service did not respond. Retrying. Cost: 25 Attempt 1 failed: The shipping service did not respond. Retrying. Attempt 2 failed: The shipping service did not respond. Retrying. Status: Delivered
Choosing where the interceptor is generated
By default, the interceptors are internal static methods of the class MetalamaInterceptors in the global namespace. To choose another location, use one of these methods:
- PlaceInStaticClass generates the interceptor in a static class of the given full name.
- PlaceInCallingType generates the interceptor as a private method of the type that contains the call site. The interceptor can then access the private members of that type. The type does not need to be
partial. - PlaceInType generates the interceptor in a given type of the current project, for instance a base class of the calling types.
A call site in top-level statements has no calling type. Use CanPlaceInCallingType in ForEachSite to choose another location for such a call site.
For instance, the following registration generates the interceptor as a private method of the calling type:
16.PlaceInCallingType() );The template can then access the private static members of the caller through meta.ThisType:
25meta.ThisType._log.WriteLine(
26 $"Calling {meta.Interception.Method.InterceptedMethod.Name}." );Shaping the generated method
Configure gives a delegate that receives an IInterceptorMethodBuilder. The delegate can change the name and the accessibility of the generated method, widen the types of its parameters, and add parameters, whose value it gives with SetArgument. The reference kinds, the positions and the return type cannot change.
Call Configure after the other methods of the registration, such as WithArgs and the placement methods, because they determine the signature that the delegate receives. After Configure, only more calls of Configure are possible.
The generated method is static by default, and its first parameter receives the receiver of the call. The delegate can instead map the receiver to this: set ReceiverMapping to InterceptorReceiverMapping.This. The generated method is then an instance method, which the call site calls on its receiver. This mapping is possible only when ReceiverEligibility says so: the interceptor must be in the calling type or in one of its base types, and the receiver must be of the calling type.
For instance, the following registration gives a template argument, and adds a parameter whose value is the name of the calling member:
17.WithArgs( new { category = "Pricing" } )
18.Configure(
19 method =>
20 {
21 var caller = method.Method.AddParameter( "caller", typeof(string) );
22 method.SetArgument(
23 caller,
24 (TypedConstant) method.Context.Caller.Member.Name );
25 } ) );Example: Writing to the caller's logger
The following aspect generates the interceptor as a private static method of the calling type, so that the template can write to the private static _log field of the caller through meta.ThisType.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4
5namespace Doc.CallerLogger;
6
7public class LogPaymentCallsAttribute : TypeAspect
8{
9 public override void BuildAspect( IAspectBuilder<INamedType> builder )
10 => builder.InterceptMethods(
11 calls => calls
12 .Type( typeof(PaymentGateway) )
13 .AllMethods()
14 .RedirectToSynthesizedMethod( nameof(this.LogCall) )
16 .PlaceInCallingType() );
18
19 [Template]
20 public dynamic? LogCall()
21 {
22 // The interceptor is a private method of the calling type, so it can read
23 // the private static field _log.
25 meta.ThisType._log.WriteLine(
26 $"Calling {meta.Interception.Method.InterceptedMethod.Name}." );
28
29 return meta.Proceed();
30 }
31}
32
1using System;
2using System.IO;
3
4namespace Doc.CallerLogger;
5
6[LogPaymentCalls]
7public class OrderService
8{
9 private static readonly TextWriter _log = Console.Out;
10
11 public string Checkout( decimal amount ) => PaymentGateway.Charge( amount, "EUR" );
12}
13
14public static class PaymentGateway
15{
16 public static string Charge( decimal amount, string currency ) => $"ch_{amount}_{currency}";
17}
18
19internal class Program
20{
21 private static void Main() => new OrderService().Checkout( 42 );
22}
23
1using System;
2using System.IO;
3
4namespace Doc.CallerLogger;
5
6[LogPaymentCalls]
7public class OrderService
8{
9 private static readonly TextWriter _log = Console.Out;
10
11 public string Checkout(decimal amount) => PaymentGateway_Charge_Interceptor(amount: amount, currency: "EUR");
12
13 private static string PaymentGateway_Charge_Interceptor(decimal amount, string currency)
14 {
15 _log.WriteLine("Calling Charge.");
16 return PaymentGateway.Charge(amount, currency);
17 }
18}
19
20public static class PaymentGateway
21{
22 public static string Charge(decimal amount, string currency) => $"ch_{amount}_{currency}";
23}
24
25internal class Program
26{
27 private static void Main() => new OrderService().Checkout(42);
28}
29
Calling Charge.
Intercepting calls to protected methods of a base class
A class that derives from a framework class, such as a Windows Forms or WPF control, often calls the protected methods that it inherits, for instance this.UpdateStyles(). Two rules apply:
- The calls are bound to the method of the framework class, so the registration must select the framework class, not the derived class. To limit the interception to the derived classes, use a query that selects them as the scope.
- A protected method can be called only from the class hierarchy, and only through an instance of the calling class. Metalama therefore generates an instance method: the call site calls it on its receiver, and it calls the intercepted method on
this. Generate it in the calling type with PlaceInCallingType, or in a common base class of your project with PlaceInType, so that all the derived classes share it. With the defaultMetalamaInterceptorsclass, the call cannot be intercepted, and Metalama reports LAMA1012.
For instance:
14.SelectTypes()
15.Where( t => t.IsConvertibleTo( typeof(AppControl) ) )
16.InterceptMethods(
17 calls => calls
18 // Select the framework class that declares the protected method.
19 .Type( typeof(Control) )
20 .Methods( "UpdateStyles" )
21 .RedirectToSynthesizedMethod( nameof(this.LogStyles) )
22 // A protected method can be called only from the class hierarchy,
23 // so the interceptor is generated in the common base class of the project.
24 .PlaceInType( typeof(AppControl) ) );Example: Intercepting calls to a protected method of a framework class
In the following example, OrderGrid and CustomerGrid derive from AppControl, the base class of the controls of the project, which derives from the framework class Control. Their calls to the protected method Control.UpdateStyles are redirected to one interceptor, generated as a private protected instance method of AppControl.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Fabrics;
5using System;
6
7namespace Doc.BaseClassCalls;
8
9internal class Fabric : ProjectFabric
10{
11 public override void AmendProject( IProjectAmender amender )
12 => amender
14 .SelectTypes()
15 .Where( t => t.IsConvertibleTo( typeof(AppControl) ) )
16 .InterceptMethods(
17 calls => calls
18 // Select the framework class that declares the protected method.
19 .Type( typeof(Control) )
20 .Methods( "UpdateStyles" )
21 .RedirectToSynthesizedMethod( nameof(this.LogStyles) )
22 // A protected method can be called only from the class hierarchy,
23 // so the interceptor is generated in the common base class of the project.
24 .PlaceInType( typeof(AppControl) ) );
26
27 [Template]
28 public dynamic? LogStyles()
29 {
30 Console.WriteLine( "Updating styles." );
31
32 return meta.Proceed();
33 }
34}
35
1namespace Doc.BaseClassCalls;
2
3// The base class of all the controls of the application.
4public abstract class AppControl : Control;
5
6public class OrderGrid : AppControl
7{
8 public void ApplyTheme() => this.UpdateStyles();
9}
10
11public class CustomerGrid : AppControl
12{
13 public void ApplyTheme() => this.UpdateStyles();
14}
15
16internal class Program
17{
18 private static void Main()
19 {
20 new OrderGrid().ApplyTheme();
21 new CustomerGrid().ApplyTheme();
22 }
23}
24
1using System;
2
3namespace Doc.BaseClassCalls;
4
5// The base class of all the controls of the application.
6public abstract class AppControl : Control
7{
8 private protected void Control_UpdateStyles_Interceptor()
9 {
10 Console.WriteLine("Updating styles.");
11 this.UpdateStyles();
12 }
13}
14
15public class OrderGrid : AppControl
16{
17 public void ApplyTheme() => this.Control_UpdateStyles_Interceptor();
18}
19
20public class CustomerGrid : AppControl
21{
22 public void ApplyTheme() => this.Control_UpdateStyles_Interceptor();
23}
24
25internal class Program
26{
27 private static void Main()
28 {
29 new OrderGrid().ApplyTheme();
30 new CustomerGrid().ApplyTheme();
31 }
32}
33
using System;
namespace Doc.BaseClassCalls;
// A base class of a UI framework, compiled in another project, like Control in Windows Forms.
public class Control
{
// A protected method: only derived classes can call it.
protected void UpdateStyles() => Console.WriteLine( $"{this.GetType().Name} styles updated." );
}Updating styles. OrderGrid styles updated. Updating styles. CustomerGrid styles updated.
Limitations
In this version, interceptors generated from templates intercept only calls. A method group converted to a delegate, such as items.Select( Normalize ), can be intercepted only by an existing method. See Choosing which calls to intercept.
In this section
| Article | Description |
|---|---|
| Writing generic interceptor templates | Reuse one template for methods of the same shape, and intercept generic methods with specialized or shared interceptors. |
| Interceptor deduplication rules | Reference: when call sites share a generated interceptor, and how granularity, names and accessibility are decided. |