Open sandboxFocusImprove this doc

Interceptor deduplication rules

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 Configure delegate 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.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Fabrics;
4using System;
5
6namespace Doc.DedupShared;
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) ) );
16
17    // The template does not read the calling code, so all the call sites share one generated
18    // method.
19    [Template]
20    public dynamic? Log()
21    {
22        Console.WriteLine( $"Calling {meta.Interception.Method.InterceptedMethod.Name}." );
23
24        return meta.Proceed();
25    }
26}
27
Source Code
1namespace Doc.DedupShared;
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
Transformed Code
1namespace Doc.DedupShared;
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_Interceptor(amount: amount) + MetalamaInterceptors.PaymentGateway_Charge_Interceptor(amount: amount);
9}
10
11public class SubscriptionService
12{
13    public string Renew(decimal fee) => MetalamaInterceptors.PaymentGateway_Charge_Interceptor(amount: fee);
14}
15
16public static class PaymentGateway
17{
18    public static string Charge(decimal amount) => $"ch_{amount}";
19}
20
using System;
using Doc.DedupShared;
internal static class MetalamaInterceptors
{
  internal static string PaymentGateway_Charge_Interceptor(decimal amount)
  {
    Console.WriteLine("Calling Charge.");
    return PaymentGateway.Charge(amount);
  }
}

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
Source Code
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
Transformed Code
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:

  1. The path of the file, relative to the project directory, with / as separator, in ordinal order.
  2. 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. With PlaceInStaticClass, 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.