Open sandboxFocusImprove this doc

Choosing which calls to intercept

An interception registration selects calls in two dimensions:

  • The scope is the code that contains the call sites: a type, a namespace, or the whole project.
  • The method selection is the set of methods whose calls are intercepted.

This article describes both dimensions, and how to decide the interception for each call site.

Choosing the calling code

From an aspect

When an aspect calls InterceptMethods on its builder, the scope is the target of the aspect. You can also register interceptors for a child declaration with builder.With( declaration ), or for several declarations with a query on builder.Outbound. In all cases, the scope must be contained in the target of the aspect.

An interceptor registration is recorded during BuildAspect, and the calls are rewritten after all aspects have run. When the aspect reports an error or is skipped, its interceptors are discarded.

For instance, the following aspect intercepts the calls to HttpClient.GetStringAsync in the type to which it is applied:

14var interceptor = typeof(HttpLogging).AsINamedType()
15    .Methods.OfName( nameof(HttpLogging.GetStringAsync) )
16    .Single();
17
18builder.InterceptMethods(
19    calls => calls
20        .Type( typeof(HttpClient) )
21        .Methods( nameof(HttpClient.GetStringAsync) )
22        .Where( m => m.Parameters is [{ Type.SpecialType: SpecialType.String }] )
23        .RedirectToExistingMethod( interceptor ) );

From a fabric

A fabric calls InterceptMethods on its amender or on a query:

  • In a ProjectFabric, amender.InterceptMethods( ... ) intercepts the calls in the whole project.
  • In a NamespaceFabric, the scope is the namespace.
  • In a TypeFabric, the scope is the type.
  • A query such as amender.SelectTypes().Where( ... ).InterceptMethods( ... ) makes each selected declaration a scope.

For instance, the following project fabric intercepts the calls to all the methods of PricingClient in the whole project:

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

With a tagged query, the delegate given to ForEachSite, described below, can read the tag of the scope through ScopeTag.

Example: Switching one module at a time

The following namespace fabric is in the Billing namespace. It redirects the calls that this namespace makes to the legacy tax calculator to the new one. The Reporting namespace still uses the legacy calculator. To migrate another module, add the same fabric to its namespace. When all the modules are migrated, change the call sites in the source code and delete the fabrics.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4using System.Linq;
5
6namespace Doc.SwitchPerModule.Billing;
7
8// A namespace fabric: its scope is the Billing namespace. Add one to each migrated module.
10internal class MigrationFabric : NamespaceFabric
11{
12    public override void AmendNamespace( INamespaceAmender amender )
13    {
14        var adapter = typeof(NewTaxCalculator).AsINamedType()
15            .Methods.OfName( nameof(NewTaxCalculator.ComputeTax) )
16            .Single( m => m.Parameters.Count == 3 );
17
18        amender.InterceptMethods(
19            calls => calls
20                .Type( typeof(LegacyTaxCalculator) )
21                .Methods( nameof(LegacyTaxCalculator.ComputeTax) )
22                .RedirectToExistingMethod( adapter ) );
23    }
24}
26
Source Code
1namespace Doc.SwitchPerModule.Billing
2{
3    // This module is migrated to the new calculator.
4    public class InvoiceService
5    {
6        private readonly LegacyTaxCalculator _calculator = new();
7
8        public decimal ComputeTotal( decimal amount, string region )
9            => amount + this._calculator.ComputeTax( amount, region );
10    }
11}
12
13namespace Doc.SwitchPerModule.Reporting
14{
15    // This module still uses the legacy calculator.
16    public class TaxReport
17    {
18        private readonly LegacyTaxCalculator _calculator = new();
19
20        public decimal ComputeTax( decimal revenue, string region )
21            => this._calculator.ComputeTax( revenue, region );
22    }
23}
24
Transformed Code
1namespace Doc.SwitchPerModule.Billing
2{
3    // This module is migrated to the new calculator.
4    public class InvoiceService
5    {
6        private readonly LegacyTaxCalculator _calculator = new();
7
8        public decimal ComputeTotal(decimal amount, string region)
9            => amount + NewTaxCalculator.ComputeTax(this._calculator, amount, region);
10    }
11}
12
13namespace Doc.SwitchPerModule.Reporting
14{
15    // This module still uses the legacy calculator.
16    public class TaxReport
17    {
18        private readonly LegacyTaxCalculator _calculator = new();
19
20        public decimal ComputeTax(decimal revenue, string region)
21            => this._calculator.ComputeTax(revenue, region);
22    }
23}
24
1namespace Doc.SwitchPerModule;
2
3public class LegacyTaxCalculator
4{
5    public decimal ComputeTax( decimal amount, string region )
6        => region == "EU" ? amount * 0.2m : amount * 0.1m;
7}
8
9public static class NewTaxCalculator
10{
11    public static decimal ComputeTax( decimal amount, string region )
12        => region switch
13        {
14            "EU" => amount * 0.2m,
15            "CH" => amount * 0.081m,
16            _ => amount * 0.1m
17        };
18
19    // Adapts the calls written for the legacy calculator. The calculator instance is not used.
20    public static decimal ComputeTax(
21        LegacyTaxCalculator calculator,
22        decimal amount,
23        string region )
24        => ComputeTax( amount, region );
25}
26

Nested types and lambdas

  • By default, the scope of a type does not include its nested types. Call IncludingNestedTypes to include them.
  • By default, the calls in lambdas, anonymous methods and local functions are included. Call ExcludingLambdas to exclude them.

Excluding code

The calls in a declaration that has the ExcludeInterceptorsAttribute attribute are never intercepted, whatever the registration. The attribute applies to the members and nested types of a type, and to the lambdas and local functions of a member. Use [ExcludeInterceptors( false )] to include a declaration again inside an excluded type.

In a fabric, the ExcludeInterceptors extension methods have the same effect on the declarations that a query selects, for instance amender.SelectTypes().Where( t => t.Name.EndsWith( "Migration" ) ).ExcludeInterceptors().

The exclusion is not inherited by derived types or overriding members.

For instance, the following interceptor calls the method that it intercepts, so its class is excluded from interception:

9[ExcludeInterceptors]
10public static class Audit

Choosing the intercepted methods

The description of the interception selects a type, then methods:

  • Type selects one type, given as a Type or as an INamedType. A generic type definition, such as typeof(List<>), selects the methods of all its constructions.
  • Types selects several types with a predicate, for instance all the types of a namespace. The methods must then be given by name, because Metalama finds the call sites by the name of the called method.
  • Methods selects the methods of the given names, with all their overloads.
  • AllMethods selects all the ordinary methods that the type declares. It does not select inherited methods, operators, accessors or constructors.
  • Where keeps only the methods that a predicate accepts, for instance one overload.

A call bound to an override of a selected method is also intercepted. Call ExcludingOverrides to intercept only the calls bound to the selected method itself. A call bound to an implementation of a selected interface method is intercepted only after IncludingInterfaceImplementations.

For instance, the following registration intercepts the calls to IOrderRepository.Save, including the calls bound to its implementation SqlOrderRepository.Save:

13calls => calls
14    .Type( typeof(IOrderRepository) )
15    .Methods( nameof(IOrderRepository.Save) )
16    .IncludingInterfaceImplementations()
17    .RedirectToSynthesizedMethod( nameof(this.LogCall) ) );

The complete example:

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.InterfaceCalls;
7
8public class LogRepositoryCallsAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11        => builder.InterceptMethods(
13            calls => calls
14                .Type( typeof(IOrderRepository) )
15                .Methods( nameof(IOrderRepository.Save) )
16                .IncludingInterfaceImplementations()
17                .RedirectToSynthesizedMethod( nameof(this.LogCall) ) );
19
20    [Template]
21    public dynamic? LogCall()
22    {
23        Console.WriteLine(
24            $"Calling {meta.Interception.Method.InterceptedMethod.ToDisplayString()}." );
25
26        return meta.Proceed();
27    }
28}
29
Source Code
1namespace Doc.InterfaceCalls;
2
3public record Order( int Id );
4
5public interface IOrderRepository
6{
7    void Save( Order order );
8}
9
10public class SqlOrderRepository : IOrderRepository
11{
12    public void Save( Order order ) { }
13}
14
15[LogRepositoryCalls]
16public class OrderService
17{
18    private readonly IOrderRepository _repository = new SqlOrderRepository();
19    private readonly SqlOrderRepository _sqlRepository = new();
20
21    // The call is bound to the interface method.
22    public void Place( Order order ) => this._repository.Save( order );
23
24    // The call is bound to the implementation.
25    public void Import( Order order ) => this._sqlRepository.Save( order );
26}
27
Transformed Code
1namespace Doc.InterfaceCalls;
2
3public record Order(int Id);
4
5public interface IOrderRepository
6{
7    void Save(Order order);
8}
9
10public class SqlOrderRepository : IOrderRepository
11{
12    public void Save(Order order) { }
13}
14
15[LogRepositoryCalls]
16public class OrderService
17{
18    private readonly IOrderRepository _repository = new SqlOrderRepository();
19    private readonly SqlOrderRepository _sqlRepository = new();
20
21    // The call is bound to the interface method.
22    public void Place(Order order) => MetalamaInterceptors.IOrderRepository_Save_Interceptor(this._repository, order: order);
23
24    // The call is bound to the implementation.
25    public void Import(Order order) => MetalamaInterceptors.SqlOrderRepository_Save_Interceptor(this._sqlRepository, order: order);
26}
27
using System;
using Doc.InterfaceCalls;
internal static class MetalamaInterceptors
{
  internal static void IOrderRepository_Save_Interceptor(IOrderRepository receiver, Order order)
  {
    Console.WriteLine("Calling IOrderRepository.Save(Order).");
    receiver.Save(order);
  }
  internal static void SqlOrderRepository_Save_Interceptor(SqlOrderRepository receiver, Order order)
  {
    Console.WriteLine("Calling SqlOrderRepository.Save(Order).");
    receiver.Save(order);
  }
}

The predicates and delegates of the description are stored with the registration and can run later, concurrently, and several times. They must not capture a declaration or a compilation.

Intercepting calls only in some builds

A fabric can register interceptors only when a condition on the project is true. The IProject object, available as amender.Project, gives:

For instance, the following fabric registers its interceptors only when the TRACE_CALLS symbol is defined:

15if ( !amender.Project.PreprocessorSymbols.Contains( "TRACE_CALLS" ) )
16{
17    return;
18}

Example: Tracing only in some builds

The following fabric traces the calls to File.ReadAllText only when the TRACE_CALLS symbol is defined. For instance, add <DefineConstants>$(DefineConstants);TRACE_CALLS</DefineConstants> to a build configuration that is never released.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Fabrics;
4using System;
5using System.IO;
6
7namespace Doc.TraceBuildOnly;
8
9internal class TracingFabric : ProjectFabric
10{
11    public override void AmendProject( IProjectAmender amender )
12    {
13        // Interceptors are registered only when the TRACE_CALLS symbol is defined.
15        if ( !amender.Project.PreprocessorSymbols.Contains( "TRACE_CALLS" ) )
16        {
17            return;
18        }
20
21        amender.InterceptMethods(
22            calls => calls
23                .Type( typeof(File) )
24                .Methods( nameof(File.ReadAllText) )
25                .RedirectToSynthesizedMethod( nameof(this.Trace) ) );
26    }
27
28    [Template]
29    public dynamic? Trace()
30    {
31        Console.WriteLine( $"Calling {meta.Interception.Method.InterceptedMethod.Name}." );
32
33        return meta.Proceed();
34    }
35}
36
Source Code
1using System.IO;
2
3namespace Doc.TraceBuildOnly;
4
5public class SettingsLoader
6{
7    public string Load( string path ) => File.ReadAllText( path );
8}
9
Transformed Code
1using System.IO;
2
3namespace Doc.TraceBuildOnly;
4
5public class SettingsLoader
6{
7    public string Load(string path) => MetalamaInterceptors.File_ReadAllText_Interceptor(path: path);
8}
9
using System;
using System.IO;
internal static class MetalamaInterceptors
{
  internal static string File_ReadAllText_Interceptor(string path)
  {
    Console.WriteLine("Calling ReadAllText.");
    return File.ReadAllText(path);
  }
}

Deciding for each call site

ForEachSite gives a delegate that decides the interception of each call site. The delegate receives an IMethodInterceptionSite and returns either site.Skip(), which leaves the call site unchanged, or a result that the site creates, such as site.RedirectToSynthesizedMethod( ... ).

The site describes the call:

  • UseKind: a call, a method group converted to a delegate or to a function pointer, or an event subscription.
  • InterceptedMethod and MatchedMethod: the called method, and the selected method that it matches.
  • Receiver and Arguments: the receiver and the arguments written at the call site. Their expressions can be inspected, for instance to read a constant value, but they cannot be copied into generated code.
  • IsResultUsed: whether the calling code uses the return value.
  • Caller: the calling member, its parameters, and whether the call is in a lambda, in an async function, or where this is available.
  • NonInterceptableReason: the reason why the call cannot be intercepted in this version, if any. Skip such call sites to avoid a warning.

The delegate can report diagnostics through Diagnostics. An exception thrown by the delegate is reported as an error, and the call site is left unchanged.

For instance, the following delegate skips the call sites in types that have the [TestHelper] attribute:

16.ForEachSite(
17    site =>
18    {
19        var callingType = site.Caller.Member.DeclaringType;
20
21        if ( callingType.Attributes.Any( typeof(TestHelperAttribute) ) )
22        {
23            return site.Skip();
24        }
25
26        return site.RedirectToSynthesizedMethod( nameof(this.LogCall) );
27    } ) );

Example: Skipping the calls from test helpers

The following aspect logs the calls to PaymentGateway, except the calls from the types that have the [TestHelper] attribute.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.SkipTestHelpers;
7
8public class LogPaymentCallsAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11        => builder.InterceptMethods(
12            calls => calls
13                .Type( typeof(PaymentGateway) )
14                .AllMethods()
16                .ForEachSite(
17                    site =>
18                    {
19                        var callingType = site.Caller.Member.DeclaringType;
20
21                        if ( callingType.Attributes.Any( typeof(TestHelperAttribute) ) )
22                        {
23                            return site.Skip();
24                        }
25
26                        return site.RedirectToSynthesizedMethod( nameof(this.LogCall) );
27                    } ) );
29
30    [Template]
31    public dynamic? LogCall()
32    {
33        Console.WriteLine( $"Calling {meta.Interception.Method.InterceptedMethod.Name}." );
34
35        return meta.Proceed();
36    }
37}
38
Source Code
1using System;
2
3namespace Doc.SkipTestHelpers;
4
5[LogPaymentCalls]
6public class CheckoutService
7{
8    public string Pay( decimal amount ) => PaymentGateway.Charge( amount );
9}
10
11[TestHelper]
12[LogPaymentCalls]
13public class FakeCheckout
14{
15    public string PayForTest() => PaymentGateway.Charge( 1 );
16}
17
18public static class PaymentGateway
19{
20    public static string Charge( decimal amount ) => $"ch_{amount}";
21}
22
23[AttributeUsage( AttributeTargets.Class )]
24public class TestHelperAttribute : Attribute;
25
Transformed Code
1using System;
2
3namespace Doc.SkipTestHelpers;
4
5[LogPaymentCalls]
6public class CheckoutService
7{
8    public string Pay(decimal amount) => MetalamaInterceptors.PaymentGateway_Charge_Interceptor(amount: amount);
9}
10
11[TestHelper]
12[LogPaymentCalls]
13public class FakeCheckout
14{
15    public string PayForTest() => PaymentGateway.Charge(1);
16}
17
18public static class PaymentGateway
19{
20    public static string Charge(decimal amount) => $"ch_{amount}";
21}
22
23[AttributeUsage(AttributeTargets.Class)]
24public class TestHelperAttribute : Attribute;
25
using System;
using Doc.SkipTestHelpers;
internal static class MetalamaInterceptors
{
  internal static string PaymentGateway_Charge_Interceptor(decimal amount)
  {
    Console.WriteLine("Calling Charge.");
    return PaymentGateway.Charge(amount);
  }
}

Handling calls that cannot be intercepted

Some call sites match a registration but cannot be intercepted in this version, for instance a call to a method that returns by reference, or a call in a lambda converted to an expression tree. For these call sites, you have two choices:

  • Let Metalama report them. When the registration returns an interceptor for such a call site, Metalama reports the warning LAMA1012, which gives the reason, and leaves the call unchanged. This is the default behavior.
  • Skip them silently. In ForEachSite, return site.Skip() when NonInterceptableReason is not None:

16.ForEachSite(
17    site => site.NonInterceptableReason != NonInterceptableReason.None
18        ? site.Skip()
19        : site.RedirectToSynthesizedMethod( nameof(this.Trace) ) ) );

Example: Skipping calls that cannot be intercepted

In the following example, the call to a method that returns by reference, and the call in an IQueryable lambda, are skipped without a warning. The call in the IEnumerable lambda is intercepted. For the list of reasons, see Interceptor limitations and troubleshooting.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.SkipNonInterceptable;
7
8public class TraceCallsAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11        => builder.InterceptMethods(
12            calls => calls
13                .Type( typeof(Store) )
14                .AllMethods()
16                .ForEachSite(
17                    site => site.NonInterceptableReason != NonInterceptableReason.None
18                        ? site.Skip()
19                        : site.RedirectToSynthesizedMethod( nameof(this.Trace) ) ) );
21
22    [Template]
23    public dynamic? Trace()
24    {
25        Console.WriteLine( $"Calling {meta.Interception.Method.InterceptedMethod.Name}." );
26
27        return meta.Proceed();
28    }
29}
30
Source Code
1using System.Collections.Generic;
2using System.Linq;
3
4namespace Doc.SkipNonInterceptable;
5
6[TraceCalls]
7public class Inventory
8{
9    public void Reset() => Store.Slot() = 0;
10
11    public IQueryable<int> FindLarge( IQueryable<int> quantities )
12        => quantities.Where( q => Store.IsLarge( q ) );
13
14    public IEnumerable<int> FindLarge( IEnumerable<int> quantities )
15        => quantities.Where( q => Store.IsLarge( q ) );
16}
17
18public static class Store
19{
20    private static int _slot;
21
22    public static ref int Slot() => ref _slot;
23
24    public static bool IsLarge( int quantity ) => quantity > 100;
25}
26
Transformed Code
1using System.Collections.Generic;
2using System.Linq;
3
4namespace Doc.SkipNonInterceptable;
5
6[TraceCalls]
7public class Inventory
8{
9    public void Reset() => Store.Slot() = 0;
10
11    public IQueryable<int> FindLarge(IQueryable<int> quantities)
12        => quantities.Where(q => Store.IsLarge(q));
13
14    public IEnumerable<int> FindLarge(IEnumerable<int> quantities)
15        => quantities.Where(q => MetalamaInterceptors.Store_IsLarge_Interceptor(quantity: q));
16}
17
18public static class Store
19{
20    private static int _slot;
21
22    public static ref int Slot() => ref _slot;
23
24    public static bool IsLarge(int quantity) => quantity > 100;
25}
26
using System;
using Doc.SkipNonInterceptable;
internal static class MetalamaInterceptors
{
  internal static bool Store_IsLarge_Interceptor(int quantity)
  {
    Console.WriteLine("Calling IsLarge.");
    return Store.IsLarge(quantity);
  }
}

Intercepting delegates and event handlers

A registration intercepts all the uses of the selected methods, not only calls. A method group converted to a delegate, such as items.Select( Normalize ), or added to an event, such as button.Click += OnClick, is also a use. The MethodUseKind enumeration lists the kinds of use.

The kinds of use have these constraints in this version:

  • A method group can be intercepted only by an existing method with exactly the same signature as the intercepted method.
  • Interceptors generated from templates support only calls. Skip the other kinds of use. Otherwise, Metalama reports an error.
  • Intercepting a method group changes the identity of the delegate. When you intercept an event subscription, intercept the matching unsubscription in the same way, otherwise -= does not remove the handler.

For instance, the following interceptor of TextUtil.Normalize has exactly the signature of the intercepted method, so it can also intercept the method group:

9    public static string Normalize( string text )

The following delegate skips all the uses except calls:

16.ForEachSite(
17    site => site.UseKind == MethodUseKind.Call
18        ? site.RedirectToSynthesizedMethod( nameof(this.LogCall) )
19        : site.Skip() ) );

Example: Intercepting a method group

The following aspect redirects the calls to TextUtil.Normalize, including the method group passed to Select, to an existing method with the same signature.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System.Linq;
5
6namespace Doc.MethodGroups;
7
8public class LogTextCallsAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11    {
12        var interceptor = typeof(TextLogging).AsINamedType()
13            .Methods.OfName( nameof(TextLogging.Normalize) )
14            .Single();
15
16        builder.InterceptMethods(
17            calls => calls
18                .Type( typeof(TextUtil) )
19                .Methods( nameof(TextUtil.Normalize) )
20                .RedirectToExistingMethod( interceptor ) );
21    }
22}
23
Source Code
1using System.Collections.Generic;
2using System.Linq;
3
4namespace Doc.MethodGroups;
5
6[LogTextCalls]
7public class TagService
8{
9    public string NormalizeOne( string tag ) => TextUtil.Normalize( tag );
10
11    public List<string> NormalizeAll( IEnumerable<string> tags )
12        => tags.Select( TextUtil.Normalize ).ToList();
13}
14
15public static class TextUtil
16{
17    public static string Normalize( string text ) => text.Trim().ToLowerInvariant();
18}
19
Transformed Code
1using System.Collections.Generic;
2using System.Linq;
3
4namespace Doc.MethodGroups;
5
6[LogTextCalls]
7public class TagService
8{
9    public string NormalizeOne(string tag) => TextLogging.Normalize(tag);
10
11    public List<string> NormalizeAll(IEnumerable<string> tags)
12        => tags.Select(TextLogging.Normalize).ToList();
13}
14
15public static class TextUtil
16{
17    public static string Normalize(string text) => text.Trim().ToLowerInvariant();
18}
19
1using System;
2
3namespace Doc.MethodGroups;
4
5public static class TextLogging
6{
7    // A method group is intercepted only by a method with exactly the same signature.
9    public static string Normalize( string text )
11    {
12        Console.WriteLine( $"Normalizing '{text}'." );
13
14        return TextUtil.Normalize( text );
15    }
16}
17

Example: Intercepting only calls

The following aspect generates its interceptor from a template, which supports only calls, so it skips the other kinds of use. The method group passed to Select is left unchanged.

1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.CallsOnly;
7
8public class LogTextCallsAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11        => builder.InterceptMethods(
12            calls => calls
13                .Type( typeof(TextUtil) )
14                .AllMethods()
16                .ForEachSite(
17                    site => site.UseKind == MethodUseKind.Call
18                        ? site.RedirectToSynthesizedMethod( nameof(this.LogCall) )
19                        : site.Skip() ) );
21
22    [Template]
23    public dynamic? LogCall()
24    {
25        Console.WriteLine( $"Calling {meta.Interception.Method.InterceptedMethod.Name}." );
26
27        return meta.Proceed();
28    }
29}
30
Source Code
1using System.Collections.Generic;
2using System.Linq;
3
4namespace Doc.CallsOnly;
5
6[LogTextCalls]
7public class TagService
8{
9    public string NormalizeOne( string tag ) => TextUtil.Normalize( tag );
10
11    public List<string> NormalizeAll( IEnumerable<string> tags )
12        => tags.Select( TextUtil.Normalize ).ToList();
13}
14
15public static class TextUtil
16{
17    public static string Normalize( string text ) => text.Trim().ToLowerInvariant();
18}
19
Transformed Code
1using System.Collections.Generic;
2using System.Linq;
3
4namespace Doc.CallsOnly;
5
6[LogTextCalls]
7public class TagService
8{
9    public string NormalizeOne(string tag) => MetalamaInterceptors.TextUtil_Normalize_Interceptor(text: tag);
10
11    public List<string> NormalizeAll(IEnumerable<string> tags)
12        => tags.Select(TextUtil.Normalize).ToList();
13}
14
15public static class TextUtil
16{
17    public static string Normalize(string text) => text.Trim().ToLowerInvariant();
18}
19
using System;
using Doc.CallsOnly;
internal static class MetalamaInterceptors
{
  internal static string TextUtil_Normalize_Interceptor(string text)
  {
    Console.WriteLine("Calling Normalize.");
    return TextUtil.Normalize(text);
  }
}

Intercepting calls in projects that use your library

A TransitiveProjectFabric in a library executes in every project that references the library. When it registers interceptors, they intercept the calls in the referencing projects. This suits a shared package that applies company-wide policies. See Adding aspects to multiple projects.

The referencing projects must build with Metalama, and each of them requires a license.

Example: A company-wide policy

The following example shows a shared package whose transitive fabric times every call to HttpClient.GetStringAsync in the projects that reference it.

Source Code
1using System.Net.Http;
2using System.Threading.Tasks;
3
4namespace Doc.CompanyPolicy;
5
6// A project of the company that references the shared package.
7public class WeatherService
8{
9    private readonly HttpClient _httpClient = new();
10
11    public Task<string> GetForecastAsync( string city )
12        => this._httpClient.GetStringAsync( $"https://weather.example.com/{city}" );
13}
14
Transformed Code
1using System.Net.Http;
2using System.Threading.Tasks;
3
4namespace Doc.CompanyPolicy;
5
6// A project of the company that references the shared package.
7public class WeatherService
8{
9    private readonly HttpClient _httpClient = new();
10
11    public Task<string> GetForecastAsync(string city)
12        => MetalamaInterceptors.HttpClient_GetStringAsync_Interceptor(this._httpClient, requestUri: $"https://weather.example.com/{city}");
13}
14
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
internal static class MetalamaInterceptors
{
  internal static async Task<string> HttpClient_GetStringAsync_Interceptor(HttpClient receiver, string? requestUri)
  {
    var stopwatch = Stopwatch.StartNew();
    try
    {
      return await receiver.GetStringAsync(requestUri);
    }
    finally
    {
      Console.WriteLine($"HTTP call took {stopwatch.ElapsedMilliseconds} ms.");
    }
  }
}
using Metalama.Extensions.Interceptors;
using Metalama.Framework.Aspects;
using Metalama.Framework.Fabrics;
using System;
using System.Diagnostics;
using System.Net.Http;

namespace Doc.CompanyPolicy;

// A shared package of the company. Its transitive fabric executes in every project that
// references the package.
public class HttpTimingPolicy : TransitiveProjectFabric
{
    public override void AmendProject( IProjectAmender amender )
        => amender.InterceptMethods(
            calls => calls
                .Type( typeof(HttpClient) )
                .Methods( nameof(HttpClient.GetStringAsync) )
                .RedirectToSynthesizedMethod( nameof(this.Time) ) );

    [Template]
    public dynamic? Time()
    {
        var stopwatch = Stopwatch.StartNew();

        try
        {
            return meta.Proceed();
        }
        finally
        {
            Console.WriteLine( $"HTTP call took {stopwatch.ElapsedMilliseconds} ms." );
        }
    }
}

Conflicts between registrations

At most one interceptor applies to a call site. When several registrations select the same call site, each of them is evaluated, and it is an error when more than one returns a result other than Skip(). To resolve the conflict, use ForEachSite in one of the registrations to skip the call sites that the other one handles.