When interceptors are generated from a template, Metalama does not generate one method per call site. The call sites that need the same code share one generated method, and the template is expanded once for this method. This article states when call sites share a method, and the consequences for the template.
These rules apply only to interceptors generated from templates. An existing method is shared by definition.
What call sites must have in common
Two call sites share a generated method when all of these are equal:
| Element | Details |
|---|---|
| Location | The class or type where the method is generated. |
| Kind of method | Static or instance, and how the receiver is passed: by value or by reference. |
| Requested name | The default name, or the name set by a Configure delegate. |
| Intercepted method | Its definition, the type arguments of the call, and how the call is dispatched. Two methods with the same signature never share an interceptor. |
| Final signature | The signature after the Configure delegates, including the explicit accessibility, the added parameters with their names and types, the widened types, the attributes and the constraints. |
| Template | The template member, and the selected variant: default, async or iterator. |
| Template provider | The object or type that declares the template. |
| Template arguments and tags | Compared by value. |
| Granularity | The calling type, member or call site, when the granularity includes it. See below. |
| Owner | The aspect instance or fabric that registered the interceptor, only with InterceptorGranularity.Owner. |
These elements do not prevent call sites from sharing a method:
- The values of the added parameters given by
SetArgument, and the type arguments of the added type parameters. They are passed at each call site. - The
Configuredelegate itself, when it produces the same signature. - How the receiver is written at the call site: implicit
this, explicit receiver, or conditional access. - How the arguments of each call are bound.
- The aspect instance that registered the interceptor, unless the granularity includes
Owner.
Example: One method for several types
In the following example, the template does not read the calling code. The four call sites, in two types, share one generated method.
Granularity
The granularity declares what the template depends on, and therefore which call sites can share a method. Set it with WithGranularity. Metalama does not detect what the template depends on.
| Granularity | The template can read | Call sites that share a method |
|---|---|---|
Global (default) |
Nothing about the calling code. | All the call sites with the same elements, in any type. |
Type |
meta.Interception.CallingType. |
The call sites of the same type. |
Member |
Also meta.Interception.CallingMember. |
The call sites of the same member. |
CallSite |
Also meta.Interception.Caller and meta.Interception.Source. |
None: each call site has its own method. |
Owner, combined with any level |
The state of the aspect instance that registered the interceptor. | The call sites of the same registration. |
The levels Type, Member and CallSite include the coarser ones. Reading a property that requires a finer level than the granularity throws an exception, whose message names the level to set.
For instance, the following registration declares that its template reads the calling member:
17.WithGranularity( InterceptorGranularity.Member ) );Example: One method per calling member
In the following example, the template reads the calling member, so the granularity is Member. Each calling member gets its own method, and the two calls in PayTwice share one.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Fabrics;
4using System;
5
6namespace Doc.DedupPerMember;
7
8internal class Fabric : ProjectFabric
9{
10 public override void AmendProject( IProjectAmender amender )
11 => amender.InterceptMethods(
12 calls => calls
13 .Type( typeof(PaymentGateway) )
14 .AllMethods()
15 .RedirectToSynthesizedMethod( nameof(this.Log) )
17 .WithGranularity( InterceptorGranularity.Member ) );
19
20 // The template reads the calling member, so each calling member gets its own generated method.
21 [Template]
22 public dynamic? Log()
23 {
25 Console.WriteLine(
26 $"{meta.Interception.CallingMember.Name} calls "
27 + $"{meta.Interception.Method.InterceptedMethod.Name}." );
29
30 return meta.Proceed();
31 }
32}
33
1namespace Doc.DedupPerMember;
2
3public class OrderService
4{
5 public string Pay( decimal amount ) => PaymentGateway.Charge( amount );
6
7 public string PayTwice( decimal amount )
8 => PaymentGateway.Charge( amount ) + PaymentGateway.Charge( amount );
9}
10
11public class SubscriptionService
12{
13 public string Renew( decimal fee ) => PaymentGateway.Charge( fee );
14}
15
16public static class PaymentGateway
17{
18 public static string Charge( decimal amount ) => $"ch_{amount}";
19}
20
1namespace Doc.DedupPerMember;
2
3public class OrderService
4{
5 public string Pay(decimal amount) => MetalamaInterceptors.PaymentGateway_Charge_Interceptor(amount: amount);
6
7 public string PayTwice(decimal amount)
8 => MetalamaInterceptors.PaymentGateway_Charge_Interceptor1(amount: amount) + MetalamaInterceptors.PaymentGateway_Charge_Interceptor1(amount: amount);
9}
10
11public class SubscriptionService
12{
13 public string Renew(decimal fee) => MetalamaInterceptors.PaymentGateway_Charge_Interceptor2(amount: fee);
14}
15
16public static class PaymentGateway
17{
18 public static string Charge(decimal amount) => $"ch_{amount}";
19}
20
using System;
using Doc.DedupPerMember;
internal static class MetalamaInterceptors
{
internal static string PaymentGateway_Charge_Interceptor(decimal amount)
{
Console.WriteLine("Pay calls Charge.");
return PaymentGateway.Charge(amount);
}
internal static string PaymentGateway_Charge_Interceptor1(decimal amount)
{
Console.WriteLine("PayTwice calls Charge.");
return PaymentGateway.Charge(amount);
}
internal static string PaymentGateway_Charge_Interceptor2(decimal amount)
{
Console.WriteLine("Renew calls Charge.");
return PaymentGateway.Charge(amount);
}
}To pass information about the call site without generating more methods, pass it as an added parameter, as in the example Added parameters of Interceptor binding rules.
For instance, the following registration adds a parameter whose value is the name of the calling member. The call sites still share one generated method:
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 } ) );The representative call site
The template of a shared method is expanded once, for one of its call sites, called the representative. The call sites are ordered by:
- The path of the file, relative to the project directory, with
/as separator, in ordinal order. - The position of the call site in the file.
The first call site is the representative. Its registration, its aspect instance and its tags are used to expand the template, and the errors reported during the expansion are located at this call site.
As a consequence, a template that reads the fields or the state of its aspect instance must use the Owner granularity. Otherwise, the call sites of other aspect instances may share a method that was expanded with the wrong instance. This is the same rule as for several instances of an aspect on one declaration. See Multiple instances of the same aspect type.
Names of the generated methods
- The default name is formed from the name of the declaring type and of the intercepted method, for instance
PaymentGateway_Charge_Interceptor. - Generated methods never overload each other. When several methods of the same location have the same requested name, a numeric suffix is added, in the order of their representatives.
- The names of inherited members are avoided.
- The default class is named
MetalamaInterceptors, in the global namespace. A suffix is added when a type of that name exists. WithPlaceInStaticClass, the namespace of the full name is created when it does not exist.
The result is the same on every machine and operating system.
Accessibility of the generated methods
Unless a Configure delegate sets the accessibility, it is computed from all the call sites of the method:
private, when every call site is in the type where the method is generated, or in a type nested in it;private protected, when call sites are in types derived from it;internal, otherwise.
When the signature of the method mentions a type that is less accessible than the method, the method cannot be generated, and an error is reported. A call site in a conditional access, such as a?.M(), is rewritten through a forwarder method, which makes the generated method internal.