Open sandboxFocusImprove this doc

Interceptor binding rules

This article states how Metalama passes the values of an intercepted call site to the interceptor: the receiver, the arguments, and the type arguments. The rules differ for an existing method, which you write, and for a method generated from a template.

In this article:

  • The intercepted method is the method that the call site calls in the source code.
  • The interceptor is the method that the rewritten call site calls.
  • The receiver is the object on which an instance method is called.
  • An added value is a value that the intercepted method does not have: a value given by WithArgs or SetArgument.

Parameters of an existing method

Each parameter of the interceptor receives the first of these values that exists. A required parameter that receives no value is an error, and the call site is not intercepted.

Order Value
1 The property of the same name of the object given to WithArgs.
2 The receiver of the call, for the first parameter, when the call has a receiver.
3 The argument of the call whose parameter in the intercepted method has the same name.
4 The caller information that a [CallerMemberName], [CallerFilePath] or [CallerLineNumber] attribute requests, for the intercepted call site.
5 The default value of the parameter.

Consequences:

  • Parameters are bound by name, not by position. When the interceptor declares its parameters in another order, the rewritten call uses named arguments.
  • WithArgs can pass the receiver or an argument of the call to a parameter of another name, through Expression or Expression.
  • An extension method called with the instance syntax, such as text.Normalize(), is bound in its static form: its receiver is the first argument.

For instance, the following interceptor of Mailer.Send( string recipient, string subject, int priority = 1 ) declares its parameters in another order, and has no parameter for priority:

29    public static void Send( string subject, string recipient )

Example: Parameters bound by name

In the following example, the interceptor declares subject and recipient in another order than the intercepted method, and it has no parameter for priority. The argument for priority is dropped, but it is still evaluated, in the order of the source code, because it can have a side effect.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.BindingByName;
8
9public class RedirectMailAttribute : TypeAspect
10{
11    public override void BuildAspect( IAspectBuilder<INamedType> builder )
12    {
13        var interceptor = typeof(MailOutbox).AsINamedType()
14            .Methods.OfName( nameof(MailOutbox.Send) )
15            .Single();
16
17        builder.InterceptMethods(
18            calls => calls
19                .Type( typeof(Mailer) )
20                .Methods( nameof(Mailer.Send) )
21                .RedirectToExistingMethod( interceptor ) );
22    }
23}
24
25public static class MailOutbox
26{
27    // The parameters are in another order, and there is no parameter for the priority.
29    public static void Send( string subject, string recipient )
31        => Console.WriteLine( $"Queued '{subject}' for {recipient}." );
32}
33
Source Code
1using System;
2
3namespace Doc.BindingByName;

4
5[RedirectMail]
6public class Newsletter
7{
8    public void SendWelcome() => Mailer.Send( "[email protected]", "Welcome" );
9
10    public void SendReport() => Mailer.Send( GetRecipient(), "Report", NextPriority() );
11
12    private static string GetRecipient()
13    {
14        Console.WriteLine( "Evaluating the recipient." );
15
16        return "[email protected]";
17    }
18
19    private static int NextPriority()
20    {
21        Console.WriteLine( "Evaluating the priority." );
22
23        return 2;
24    }
25}
26
27public static class Mailer
28{
29    public static void Send( string recipient, string subject, int priority = 1 )
30        => Console.WriteLine( $"Sending '{subject}' to {recipient} with priority {priority}." );
31}
32
33internal class Program
34{
35    private static void Main()
36    {
37        var newsletter = new Newsletter();
38        newsletter.SendWelcome();
39        newsletter.SendReport();
40    }
41}
42
Transformed Code
1using System;
2using Metalama.Framework.RunTime;
3
4namespace Doc.BindingByName;
5
6[RedirectMail]
7public class Newsletter
8{
9    public void SendWelcome() => MailOutbox.Send(recipient: "[email protected]", subject: "Welcome");
10
11    public void SendReport() => MailOutbox.Send(recipient: GetRecipient(), subject: CallSiteHelper.DropAfter("Report", NextPriority()));
12
13    private static string GetRecipient()
14    {
15        Console.WriteLine("Evaluating the recipient.");
16
17        return "[email protected]";
18    }
19
20    private static int NextPriority()
21    {
22        Console.WriteLine("Evaluating the priority.");
23
24        return 2;
25    }
26}
27
28public static class Mailer
29{
30    public static void Send(string recipient, string subject, int priority = 1)
31        => Console.WriteLine($"Sending '{subject}' to {recipient} with priority {priority}.");
32}
33
34internal class Program
35{
36    private static void Main()
37    {
38        var newsletter = new Newsletter();
39        newsletter.SendWelcome();
40        newsletter.SendReport();
41    }
42}
43
Queued 'Welcome' for [email protected].
Evaluating the recipient.
Evaluating the priority.
Queued 'Report' for [email protected].

Values accepted by WithArgs

The argument of WithArgs gives values by name. It can be:

  • an anonymous object, such as new { category = "Sales" };
  • any other object: its public instance properties and fields are bound by name;
  • an IReadOnlyDictionary<string, object?>, such as a Dictionary<string, object?>: its entries are bound by key, which is useful when the names are computed;
  • an IObjectReader.

Each value can be:

  • A literal: null, a Boolean, a character, a number, a string, or a value of an enumeration that compile-time code can use.
  • A Type, which is written as typeof.
  • A TypedConstant, for instance an array, or a value of an enumeration that only run-time code can use.
  • An expression of the call site: the receiver or an argument. It is evaluated once, in the order of the source code.
  • Another IExpression, written at the call site, for instance a parameter of the calling member given by Parameters, or the calling object given by This when CanAccessThis is true.

A name that matches no parameter and no type parameter is an error. A value that does not convert to the type of the parameter is an error. An argument of the call can be passed to two parameters only when it is passed by value and its evaluation has no side effect.

Example: Values given by an anonymous object

The following aspect gives the values of the additional parameters of the interceptor with an anonymous object: a string, a number, a type and null.

23.WithArgs(
24    new
25    {
26        category = "Sales",              // A string literal.
27        weight = 2,                      // A number.
28        source = typeof(OrderService),   // A type, written as typeof.
29        unit = (string?) null            // null, typed because an anonymous
30                                         // type needs a type.
31    } ) );

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.BindingWithArgsAnonymous;
8
9public class AddMetricContextAttribute : TypeAspect
10{
11    public override void BuildAspect( IAspectBuilder<INamedType> builder )
12    {
13        var interceptor = typeof(MetricsWithContext).AsINamedType()
14            .Methods.OfName( nameof(MetricsWithContext.Increment) )
15            .Single();
16
17        builder.InterceptMethods(
18            calls => calls
19                .Type( typeof(Metrics) )
20                .Methods( nameof(Metrics.Increment) )
21                .RedirectToExistingMethod( interceptor )
23                .WithArgs(
24                    new
25                    {
26                        category = "Sales",              // A string literal.
27                        weight = 2,                      // A number.
28                        source = typeof(OrderService),   // A type, written as typeof.
29                        unit = (string?) null            // null, typed because an anonymous
30                                                         // type needs a type.
31                    } ) );
33    }
34}
35
36public static class MetricsWithContext
37{
38    public static void Increment(
39        string name,
40        string category,
41        int weight,
42        Type source,
43        string? unit )
44        => Metrics.Increment(
45            $"{category}.{name} x{weight} ({source.Name}{(unit == null ? "" : ", " + unit)})" );
46}
47
Source Code
1using System;
2
3namespace Doc.BindingWithArgsAnonymous;
4
5[AddMetricContext]
6public class OrderService
7{
8    public void PlaceOrder() => Metrics.Increment( "orders.placed" );
9}
10
11public static class Metrics
12{
13    public static void Increment( string name ) => Console.WriteLine( $"{name} +1" );
14}
15
16internal class Program
17{
18    private static void Main() => new OrderService().PlaceOrder();
19}
20
Transformed Code
1using System;
2
3namespace Doc.BindingWithArgsAnonymous;
4
5[AddMetricContext]
6public class OrderService
7{
8    public void PlaceOrder() => MetricsWithContext.Increment(name: "orders.placed", category: "Sales", weight: 2, source: typeof(OrderService), unit: default);
9}
10
11public static class Metrics
12{
13    public static void Increment(string name) => Console.WriteLine($"{name} +1");
14}
15
16internal class Program
17{
18    private static void Main() => new OrderService().PlaceOrder();
19}
20
Sales.orders.placed x2 (OrderService) +1

Example: Values given by a dictionary

The following aspect gives the same values with a dictionary, which is useful when the names are computed.

24.WithArgs(
25    new Dictionary<string, object?>
26    {
27        ["category"] = "Sales",            // A string literal.
28        ["weight"] = 2,                    // A number.
29        ["source"] = typeof(OrderService), // A type, written as typeof.
30        ["unit"] = null                    // null.
31    } ) );

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Collections.Generic;
6using System.Linq;
7
8namespace Doc.BindingWithArgs;
9
10public class AddMetricContextAttribute : TypeAspect
11{
12    public override void BuildAspect( IAspectBuilder<INamedType> builder )
13    {
14        var interceptor = typeof(MetricsWithContext).AsINamedType()
15            .Methods.OfName( nameof(MetricsWithContext.Increment) )
16            .Single();
17
18        builder.InterceptMethods(
19            calls => calls
20                .Type( typeof(Metrics) )
21                .Methods( nameof(Metrics.Increment) )
22                .RedirectToExistingMethod( interceptor )
24                .WithArgs(
25                    new Dictionary<string, object?>
26                    {
27                        ["category"] = "Sales",            // A string literal.
28                        ["weight"] = 2,                    // A number.
29                        ["source"] = typeof(OrderService), // A type, written as typeof.
30                        ["unit"] = null                    // null.
31                    } ) );
33    }
34}
35
36public static class MetricsWithContext
37{
38    public static void Increment(
39        string name,
40        string category,
41        int weight,
42        Type source,
43        string? unit )
44        => Metrics.Increment(
45            $"{category}.{name} x{weight} ({source.Name}{(unit == null ? "" : ", " + unit)})" );
46}
47
Source Code
1using System;
2
3namespace Doc.BindingWithArgs;
4
5[AddMetricContext]
6public class OrderService
7{
8    public void PlaceOrder() => Metrics.Increment( "orders.placed" );
9}
10
11public static class Metrics
12{
13    public static void Increment( string name ) => Console.WriteLine( $"{name} +1" );
14}
15
16internal class Program
17{
18    private static void Main() => new OrderService().PlaceOrder();
19}
20
Transformed Code
1using System;
2
3namespace Doc.BindingWithArgs;
4
5[AddMetricContext]
6public class OrderService
7{
8    public void PlaceOrder() => MetricsWithContext.Increment(name: "orders.placed", category: "Sales", weight: 2, source: typeof(OrderService), unit: default);
9}
10
11public static class Metrics
12{
13    public static void Increment(string name) => Console.WriteLine($"{name} +1");
14}
15
16internal class Program
17{
18    private static void Main() => new OrderService().PlaceOrder();
19}
20
Sales.orders.placed x2 (OrderService) +1

Passing a parameter of the calling member

When a lambda or a local function that contains the call site declares a variable with the name of a parameter of the calling member that you pass, Metalama renames the variable and reports the warning LAMA0661. A parameter cannot be passed from a static lambda. A parameter passed by reference cannot be passed from a lambda. Check IsInNestedFunction and IsInStaticFunction first, as in the following snippet:

14    builder.InterceptMethods(
15        calls => calls
16            .Type( typeof(AuditLog) )
17            .Methods( nameof(AuditLog.Write) )
18            .ForEachSite(
19                site =>
20                {
21                    var requestId = site.Caller.Parameters
22                        .FirstOrDefault( p => p.Name == "requestId" );
23
24                    // A static lambda cannot read the parameter, and a lambda cannot capture
25                    // a parameter passed by reference.
26                    if ( requestId == null
27                         || (site.Caller.IsInNestedFunction
28                             && (site.Caller.IsInStaticFunction
29                                 || requestId.RefKind != RefKind.None)) )
30                    {
31                        return site.Skip();
32                    }
33
34                    return site
35                        .RedirectToExistingMethod(
36                            typeof(AuditLogWithRequest).AsINamedType()
37                                .Methods.OfName( nameof(AuditLogWithRequest.Write) )
38                                .Single() )
39                        .WithArgs( new { requestId } );
40                } ) );

For the complete example, see Passing a parameter of the calling method in Redirecting calls to existing methods.

Evaluation of the arguments

  • The arguments of the call are always evaluated in the order of the source code, even when the interceptor receives them in another order.
  • An argument that the interceptor does not receive is dropped. When its evaluation can have a side effect, it is still evaluated.

Omitted optional arguments and params arrays

When the call omits an optional argument, and the interceptor receives it, Metalama writes the default value of the intercepted method explicitly, for instance an enumeration member by its name. When the default value cannot be written in C#, the call site is not intercepted, unless the interceptor has no parameter for this argument. The optional parameters of the interceptor that have no match receive their own default value.

When the call passes the elements of a params collection:

  • If the interceptor has a params parameter of the same type, the elements are kept as written.
  • Otherwise, the elements are packed into a collection expression.
  • A collection passed in normal form, or null, is passed as it is.

For instance, the following interceptor of Log.Write( string format, Severity severity = Severity.Info, params object[] args ) has no default value and no params modifier:

29    public static void Write( string format, Severity severity, object[] args )

Example: Default values and params arrays

In the following example, the interceptor has no default value for severity, and its args parameter is a plain array. The omitted severity argument is written as Severity.Info, and the elements of the params array are packed into a collection expression.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.BindingDefaultsAndParams;
8
9public class RedirectLoggingAttribute : TypeAspect
10{
11    public override void BuildAspect( IAspectBuilder<INamedType> builder )
12    {
13        var interceptor = typeof(StructuredLog).AsINamedType()
14            .Methods.OfName( nameof(StructuredLog.Write) )
15            .Single();
16
17        builder.InterceptMethods(
18            calls => calls
19                .Type( typeof(Log) )
20                .Methods( nameof(Log.Write) )
21                .RedirectToExistingMethod( interceptor ) );
22    }
23}
24
25public static class StructuredLog
26{
27    // The severity has no default value, and the arguments are a plain array.
29    public static void Write( string format, Severity severity, object[] args )
31        => Console.WriteLine( $"[{severity}] {format} ({args.Length} arguments)" );
32}
33
Source Code
1using System;
2
3namespace Doc.BindingDefaultsAndParams;
4
5[RedirectLogging]
6public class OrderProcessor
7{
8    public void Process( int orderId )
9    {
10        Log.Write( "Processing an order." );
11        Log.Write( "Order {0} shipped to {1}.", Severity.Warning, orderId, "Paris" );
12    }
13}
14
15public enum Severity
16{
17    Info,
18    Warning
19}
20
21public static class Log
22{
23    public static void Write(
24        string format,
25        Severity severity = Severity.Info,
26        params object[] args )
27        => Console.WriteLine( $"{severity}: {string.Format( format, args )}" );
28}
29
Transformed Code
1using System;
2
3namespace Doc.BindingDefaultsAndParams;
4
5[RedirectLogging]
6public class OrderProcessor
7{
8    public void Process(int orderId)
9    {
10        StructuredLog.Write(format: "Processing an order.", args: [], severity: Severity.Info);
11        StructuredLog.Write(format: "Order {0} shipped to {1}.", severity: Severity.Warning, args: [orderId, "Paris"]);
12    }
13}
14
15public enum Severity
16{
17    Info,
18    Warning
19}
20
21public static class Log
22{
23    public static void Write(
24        string format,
25        Severity severity = Severity.Info,
26        params object[] args)
27        => Console.WriteLine($"{severity}: {string.Format(format, args)}");
28}
29

Conversions and return values

  • Each argument must convert implicitly to the type of the parameter that receives it. When the call site converts an argument to the parameter type of the intercepted method, the argument is cast to this type before it is passed.
  • A lambda argument is cast to the delegate type of the intercepted method, which keeps the meaning of the source code.
  • The return value of the interceptor must convert implicitly to the return type of the intercepted method. It is cast when the calling code uses the value. In a conditional access, such as a?.M(), the return types must be identical.
  • An interceptor that returns a value can intercept a method that returns void.

Reference kinds

  • A ref, out or in parameter must receive an argument of the call with the same reference kind and an identical type.
  • Combinations that would cause a compiler warning are refused, for instance a ref readonly parameter for an argument passed by value.

Struct receivers

When the intercepted method is an instance method of a struct, the interceptor must receive the receiver in a way that keeps the behavior of the original call.

For instance, Counter.Increment() modifies the struct, so the interceptor receives the counter by reference:

29    public static void Increment( ref Counter counter )
Receiver Interceptor parameter
A variable, and a method that can modify the struct ref. Receiving it by value would lose the modification, so it is an error.
A readonly method, or a member of a readonly struct By value or in.
A read-only variable, such as a readonly field or an in parameter, on which C# calls the method on a copy By value. ref would modify the variable, so it is an error.

Example: A mutating struct method

In the following example, Counter.Increment modifies the struct, so the interceptor receives the counter with ref. The rewritten call passes the field by reference, and the modification is kept.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.BindingStructReceiver;
8
9public class TraceCountersAttribute : TypeAspect
10{
11    public override void BuildAspect( IAspectBuilder<INamedType> builder )
12    {
13        var interceptor = typeof(CounterTracing).AsINamedType()
14            .Methods.OfName( nameof(CounterTracing.Increment) )
15            .Single();
16
17        builder.InterceptMethods(
18            calls => calls
19                .Type( typeof(Counter) )
20                .Methods( nameof(Counter.Increment) )
21                .RedirectToExistingMethod( interceptor ) );
22    }
23}
24
25public static class CounterTracing
26{
27    // Increment modifies the struct, so the receiver must be passed by reference.
29    public static void Increment( ref Counter counter )
31    {
32        counter.Add( 1 );
33        Console.WriteLine( $"Counter incremented to {counter.Value}." );
34    }
35}
36
Source Code
1using System;
2
3namespace Doc.BindingStructReceiver;
4
5[TraceCounters]
6public class Statistics
7{
8    private Counter _visits;
9
10    public int RecordVisit()
11    {
12        this._visits.Increment();
13
14        return this._visits.Value;
15    }
16}
17
18public struct Counter
19{
20    public int Value { get; private set; }
21
22    public void Increment() => this.Value++;
23
24    public void Add( int amount ) => this.Value += amount;
25}
26
27internal class Program
28{
29    private static void Main()
30    {
31        var statistics = new Statistics();
32        statistics.RecordVisit();
33        Console.WriteLine( $"Visits: {statistics.RecordVisit()}" );
34    }
35}
36
Transformed Code
1using System;
2
3namespace Doc.BindingStructReceiver;
4
5[TraceCounters]
6public class Statistics
7{
8    private Counter _visits;
9
10    public int RecordVisit()
11    {
12        CounterTracing.Increment(ref this._visits);
13
14        return this._visits.Value;
15    }
16}
17
18public struct Counter
19{
20    public int Value { get; private set; }
21
22    public void Increment() => this.Value++;
23
24    public void Add(int amount) => this.Value += amount;
25}
26
27internal class Program
28{
29    private static void Main()
30    {
31        var statistics = new Statistics();
32        statistics.RecordVisit();
33        Console.WriteLine($"Visits: {statistics.RecordVisit()}");
34    }
35}
36
Counter incremented to 1.
Counter incremented to 2.
Visits: 2

Requirements on an existing method

  • The method must be static and accessible from the call site.
  • It must not be a [Conditional] method, and it must not be the intercepted method itself.
  • A receiver whose type is a type parameter cannot be passed to an existing method in this version.
  • A base call to a virtual method cannot be intercepted by an existing method.
  • For a method group, the existing method must have exactly the signature of the intercepted method, and WithArgs can bind only type parameters.

Type parameters of an existing method

Each type parameter of the interceptor, and of the types that declare it, receives the first of these types that exists. A type parameter that receives no type is an error.

Order Type
1 The property of the same name of the object given to WithArgs, as a Type or an IType.
2 The type argument of the type parameter of the same name of the intercepted method or of the types that declare it.

Pass the definition of a generic method or of a method of a generic type: its type parameters are resolved for each call site. When a type argument cannot be written, such as an anonymous type, the rewritten call omits the type arguments, and the compiler infers them.

For instance, the type parameter T of the following interceptor binds to the type parameter T of Cache.Get<T>:

29    public static T? Get<T>( string key )

Example: A generic interceptor

In the following example, the type parameter T of the interceptor is bound to the type parameter T of Cache.Get<T>.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.BindingGeneric;
8
9public class TraceCacheAttribute : TypeAspect
10{
11    public override void BuildAspect( IAspectBuilder<INamedType> builder )
12    {
13        var interceptor = typeof(CacheTracing).AsINamedType()
14            .Methods.OfName( nameof(CacheTracing.Get) )
15            .Single();
16
17        builder.InterceptMethods(
18            calls => calls
19                .Type( typeof(Cache) )
20                .Methods( nameof(Cache.Get) )
21                .RedirectToExistingMethod( interceptor ) );
22    }
23}
24
25public static class CacheTracing
26{
27    // The type parameter T is bound by name to the type parameter T of Cache.Get.
29    public static T? Get<T>( string key )
31    {
32        Console.WriteLine( $"Reading {key} as {typeof(T).Name}." );
33
34        return Cache.Get<T>( key );
35    }
36}
37
Source Code
1using System.Collections.Generic;
2
3namespace Doc.BindingGeneric;
4
5[TraceCache]
6public class ProductCatalog
7{
8    public string? GetName( int id ) => Cache.Get<string>( $"name:{id}" );
9
10    public decimal GetPrice( int id ) => Cache.Get<decimal>( $"price:{id}" );
11}
12
13public static class Cache
14{
15    private static readonly Dictionary<string, object?> _items = new();
16
17    public static T? Get<T>( string key )
18        => _items.TryGetValue( key, out var value ) ? (T?) value : default;
19}
20
Transformed Code
1using System.Collections.Generic;
2
3namespace Doc.BindingGeneric;
4
5[TraceCache]
6public class ProductCatalog
7{
8    public string? GetName(int id) => CacheTracing.Get<string>($"name:{id}");
9
10    public decimal GetPrice(int id) => CacheTracing.Get<decimal>($"price:{id}");
11}
12
13public static class Cache
14{
15    private static readonly Dictionary<string, object?> _items = new();
16
17    public static T? Get<T>(string key)
18        => _items.TryGetValue(key, out var value) ? (T?)value : default;
19}
20

Parameters of a generated method

A method generated from a template has this signature, in this order:

  1. A receiver parameter, when the intercepted method has a receiver and the receiver is not mapped to this.
  2. One parameter for each parameter of the intercepted method, with the same name and reference kind, without default value and without params.
  3. The added parameters.

The run-time parameters of the template bind to the parameters of the generated method by name, or else by position after the receiver parameter. A run-time parameter that binds to no parameter receives its default value. Without a default value, it is an error.

WithArgs gives:

  • the values of the compile-time parameters and type parameters of the template;
  • for a run-time parameter of the template that has no match, a new added parameter, with its value at each call site;
  • the values of the parameters that a Configure delegate adds.

A Configure delegate can also add a parameter and give its value at each call site with SetArgument, which can depend on the call site. A parameter cannot receive a value from both WithArgs and SetArgument.

For instance, the following template has two run-time parameters that the intercepted method does not have:

33public dynamic? Trace( string category, string caller )

category receives its value from WithArgs, and caller from the Configure delegate:

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    } ) );

When the delegate widens the type of a parameter, meta.Proceed() casts it back to the type of the intercepted method.

Example: Added parameters

In the following example, category is a run-time parameter of the template whose value is given by WithArgs, and caller is a parameter added by the Configure delegate. Both become parameters of the generated method, and each call site passes its own value. The call sites still share one generated method.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.TemplateAddedParameter;
7
8public class TracePricingAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11        => builder.InterceptMethods(
12            calls => calls
13                .Type( typeof(PricingClient) )
14                .AllMethods()
15                .RedirectToSynthesizedMethod( nameof(this.Trace) )
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                    } ) );
27
28    // category is a run-time parameter given by WithArgs; caller is added by the Configure
29    // delegate.
30    // Both become parameters of the generated method, whose value each call site passes.
31    [Template]
33    public dynamic? Trace( string category, string caller )
35    {
36        Console.WriteLine(
37            $"[{category}] {caller} calls {meta.Interception.Method.InterceptedMethod.Name}." );
38
39        return meta.Proceed();
40    }
41}
42
Source Code
1namespace Doc.TemplateAddedParameter;
2
3[TracePricing]
4public class Cart
5{
6    public decimal Total { get; private set; }
7
8    public void Add( string sku, int quantity )
9        => this.Total += PricingClient.GetPrice( sku, quantity );
10
11    public void AddGift( string sku ) => this.Total += PricingClient.GetPrice( sku, 1 ) * 0.5m;
12}
13
14public static class PricingClient
15{
16    public static decimal GetPrice( string sku, int quantity ) => 10m * quantity;
17}
18
Transformed Code
1namespace Doc.TemplateAddedParameter;
2
3[TracePricing]
4public class Cart
5{
6    public decimal Total { get; private set; }
7
8    public void Add(string sku, int quantity)
9        => this.Total += MetalamaInterceptors.PricingClient_GetPrice_Interceptor(sku: sku, quantity: quantity, category: "Pricing", caller: "Add");
10
11    public void AddGift(string sku) => this.Total += MetalamaInterceptors.PricingClient_GetPrice_Interceptor(sku: sku, quantity: 1, category: "Pricing", caller: "AddGift") * 0.5m;
12}
13
14public static class PricingClient
15{
16    public static decimal GetPrice(string sku, int quantity) => 10m * quantity;
17}
18
using System;
using Doc.TemplateAddedParameter;
internal static class MetalamaInterceptors
{
  internal static decimal PricingClient_GetPrice_Interceptor(string sku, int quantity, string category, string caller)
  {
    Console.WriteLine($"[{category}] {caller} calls GetPrice.");
    return PricingClient.GetPrice(sku, quantity);
  }
}

Type parameters of a generated method

  • When the template has run-time type parameters, they bind to the type parameters of the intercepted method, and the generated method is generic.
  • Otherwise, the generated method is specialized for the type arguments of the call site, which a compile-time type parameter of the template can receive through WithArgs.
  • A type parameter of the calling type that the generated method cannot name becomes a type parameter of the generated method.
  • A Configure delegate can add type parameters, and give their type arguments with SetTypeArgument.

For the use cases and examples, see Writing generic interceptor templates.

Receiver of a generated method

The generated method receives the receiver of the call in one of two ways, given by ReceiverMapping:

  • Parameter, the default: the generated method is static, and its first parameter receives the receiver. It is None when the intercepted method is static.
  • This: the generated method is an instance method. The call site calls it on its receiver, for instance grid.Update_Interceptor( 3 ) or grid?.Update_Interceptor( 3 ), and meta.Proceed() calls the intercepted method on this.

The mapping to this is possible when all these conditions are true:

  • The interceptor is in the calling type or in one of its base types.
  • The intercepted method is an instance method, and not an extension method.
  • The receiver is of the calling type, and it is not base.

The mapping to this is required for a protected or private protected method of a base class of the calling type, because a static method cannot call it. When the mapping is required but not possible, the call site is not intercepted, and Metalama reports LAMA1012.

ReceiverEligibility tells a Configure delegate whether the mapping is possible for the call site, possible for all the call sites that share the generated method, or required.

What a Configure delegate can change

Can change Cannot change
The name of the method and of its parameters. The template binds to the new names. The reference kinds of the parameters.
The accessibility, within the limits of the call sites. The positions of the receiver and argument parameters.
The mapping of the receiver, from Parameter to This and back, when the mapping to this is possible and not required. The method is an instance method if and only if the mapping is This. The return type.
The type of a parameter, to a type to which the argument converts implicitly. A parameter passed by reference keeps its type. The names of the derived type parameters.
The type of the receiver parameter, for instance to an interface of the receiver.
Attributes, added parameters and added type parameters.

An attempt to change a locked part throws an exception, which is reported as an error.