The simplest interceptor is an ordinary static method that you write. Metalama rewrites the matching call sites so that they call your method instead of the original one. Your method is plain C#: you can debug it and unit-test it like any other code.
Use an existing method when you intercept a few methods, when the interceptor contains logic that you want to test on its own, or when you route calls to another implementation. When you need the same logic for many methods with different signatures, generate the interceptors from a template instead. See Generating interceptors with T# templates.
Redirecting calls from an aspect
To intercept the calls made by the members of one type, register the interceptor from an aspect:
- Add a reference to the
Metalama.Extensions.Interceptorspackage to the project that contains the aspect. - Write a static method whose parameters match the intercepted method. When the intercepted method is an instance method, the first parameter receives the object on which the method is called, called the receiver.
- In the BuildAspect method of the aspect, call the InterceptMethods extension method. Its argument is a delegate that describes the interception:
- Type selects the type that declares the intercepted methods.
- Methods selects the methods by name. A name selects every overload of that name, and Where keeps only some of them.
- RedirectToExistingMethod gives your method.
The scope of the registration is the target of the aspect: only the calls written in the members of this type are intercepted.
For instance, the following interceptor of HttpClient.GetStringAsync( string ) receives the HttpClient as its first parameter:
10 public static async Task<string> GetStringAsync( HttpClient client, string? requestUri )
11 {
12 Console.WriteLine( $"GET {requestUri}" );
13
14 var result = await client.GetStringAsync( requestUri );
15
16 Console.WriteLine( $"Received {result.Length} characters." );
17
18 return result;
19 }The aspect registers it:
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 ) );Example: Logging calls to HttpClient
The following aspect logs the calls that OrderService makes to HttpClient.GetStringAsync( string ). The Where predicate keeps the overload that takes a string, because the interceptor has a string parameter.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System.Linq;
5using System.Net.Http;
6
7namespace Doc.LogHttpCalls;
8
9public class LogHttpCallsAttribute : TypeAspect
10{
11 public override void BuildAspect( IAspectBuilder<INamedType> builder )
12 {
14 var interceptor = typeof(HttpLogging).AsINamedType()
15 .Methods.OfName( nameof(HttpLogging.GetStringAsync) )
16 .Single();
17
18 builder.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 ) );
25 }
26}
27
1using System.Net.Http;
2using System.Threading.Tasks;
3
4namespace Doc.LogHttpCalls;
5
6[LogHttpCalls]
7public class OrderService
8{
9 private readonly HttpClient _httpClient = new();
10
11 public Task<string> GetOrderAsync( int id )
12 => this._httpClient.GetStringAsync( $"https://api.example.com/orders/{id}" );
13
14 public Task<string> GetCustomerAsync( string name )
15 => this._httpClient.GetStringAsync( $"https://api.example.com/customers/{name}" );
16}
17
1using System.Net.Http;
2using System.Threading.Tasks;
3
4namespace Doc.LogHttpCalls;
5
6[LogHttpCalls]
7public class OrderService
8{
9 private readonly HttpClient _httpClient = new();
10
11 public Task<string> GetOrderAsync(int id)
12 => HttpLogging.GetStringAsync(this._httpClient, $"https://api.example.com/orders/{id}");
13
14 public Task<string> GetCustomerAsync(string name)
15 => HttpLogging.GetStringAsync(this._httpClient, $"https://api.example.com/customers/{name}");
16}
17
1using System;
2using System.Net.Http;
3using System.Threading.Tasks;
4
5namespace Doc.LogHttpCalls;
6
7public static class HttpLogging
8{
10 public static async Task<string> GetStringAsync( HttpClient client, string? requestUri )
11 {
12 Console.WriteLine( $"GET {requestUri}" );
13
14 var result = await client.GetStringAsync( requestUri );
15
16 Console.WriteLine( $"Received {result.Length} characters." );
17
18 return result;
19 }
21}
22
Redirecting calls from a project or namespace fabric
To intercept the calls made by many types, register the interceptor from a fabric, and call InterceptMethods on the amender:
- In a ProjectFabric, the scope is the whole project.
- In a NamespaceFabric, the scope is the namespace of the fabric.
To select other scopes, such as the types that a query selects, see Choosing which calls to intercept.
For instance, the following project fabric redirects the calls to Thread.Sleep( int ) in the whole project:
14var interceptor = typeof(VirtualClock).AsINamedType()
15 .Methods.OfName( nameof(VirtualClock.Sleep) )
16 .Single();
17
18amender.InterceptMethods(
19 calls => calls
20 .Type( typeof(Thread) )
21 .Methods( nameof(Thread.Sleep) )
22 .Where( m => m.Parameters is [{ Type.SpecialType: SpecialType.Int32 }] )
23 .RedirectToExistingMethod( interceptor ) );Example: Replacing Thread.Sleep in tests
Code that calls Thread.Sleep directly makes tests slow. The following project fabric redirects the calls to Thread.Sleep( int ) to a virtual clock, which records the time instead of waiting. The program waits 6 seconds in virtual time and completes immediately.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4using System.Linq;
5using System.Threading;
6
7namespace Doc.FakeSleep;
8
9internal class Fabric : ProjectFabric
10{
11 public override void AmendProject( IProjectAmender amender )
12 {
14 var interceptor = typeof(VirtualClock).AsINamedType()
15 .Methods.OfName( nameof(VirtualClock.Sleep) )
16 .Single();
17
18 amender.InterceptMethods(
19 calls => calls
20 .Type( typeof(Thread) )
21 .Methods( nameof(Thread.Sleep) )
22 .Where( m => m.Parameters is [{ Type.SpecialType: SpecialType.Int32 }] )
23 .RedirectToExistingMethod( interceptor ) );
25 }
26}
27
1using System.Threading;
2
3namespace Doc.FakeSleep;
4
5public static class Backoff
6{
7 public static void WaitBeforeAttempt( int attempt ) => Thread.Sleep( 1000 * attempt );
8}
9
1using System.Threading;
2
3namespace Doc.FakeSleep;
4
5public static class Backoff
6{
7 public static void WaitBeforeAttempt(int attempt) => VirtualClock.Sleep(1000 * attempt);
8}
9
1using System;
2
3namespace Doc.FakeSleep;
4
5internal class Program
6{
7 private static void Main()
8 {
9 for ( var attempt = 1; attempt <= 3; attempt++ )
10 {
11 Backoff.WaitBeforeAttempt( attempt );
12 }
13
14 Console.WriteLine( $"Virtual time elapsed: {VirtualClock.Elapsed.TotalSeconds} s." );
15 }
16}
17
Virtual time elapsed: 6 s.
1using System;
2
3namespace Doc.FakeSleep;
4
5public static class VirtualClock
6{
7 public static TimeSpan Elapsed { get; private set; }
8
9 public static void Sleep( int millisecondsTimeout )
10 => Elapsed += TimeSpan.FromMilliseconds( millisecondsTimeout );
11}
12
In a real project, register such an interceptor only in a test build, as described in Choosing which calls to intercept. The tests then run a binary that differs from the release build. Keep this difference small. Once the tests are in place, introduce a real abstraction, such as TimeProvider.
Example: Intercepting calls in one namespace
The following namespace fabric is in the Billing namespace. It redirects the calls to File.ReadAllText in this namespace only. The same call in the Reporting namespace is not intercepted.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4using System.IO;
5using System.Linq;
6
7namespace Doc.NamespaceScope.Billing
8{
9 // A namespace fabric intercepts only the calls in its namespace.
11 internal class Fabric : NamespaceFabric
12 {
13 public override void AmendNamespace( INamespaceAmender amender )
14 {
15 var interceptor = typeof(AuditedFile).AsINamedType()
16 .Methods.OfName( nameof(AuditedFile.ReadAllText) )
17 .Single();
18
19 amender.InterceptMethods(
20 calls => calls
21 .Type( typeof(File) )
22 .Methods( nameof(File.ReadAllText) )
23 .Where( m => m.Parameters.Count == 1 )
24 .RedirectToExistingMethod( interceptor ) );
25 }
26 }
28}
29
30namespace Doc.NamespaceScope
31{
32 // The interceptor is outside the Billing namespace, so its own call to File.ReadAllText
33 // is not intercepted.
34 public static class AuditedFile
35 {
36 public static string ReadAllText( string path )
37 {
38 System.Console.WriteLine( $"Reading {Path.GetFileName( path )}." );
39
40 return File.ReadAllText( path );
41 }
42 }
43}
44
1using System.IO;
2
3namespace Doc.NamespaceScope.Billing
4{
5 // The calls in this namespace are intercepted.
6 public class InvoiceLoader
7 {
8 public string Load( string path ) => File.ReadAllText( path );
9 }
10}
11
12namespace Doc.NamespaceScope.Reporting
13{
14 // The calls in this namespace are not intercepted.
15 public class ReportLoader
16 {
17 public string Load( string path ) => File.ReadAllText( path );
18 }
19}
20
1using System.IO;
2
3namespace Doc.NamespaceScope.Billing
4{
5 // The calls in this namespace are intercepted.
6 public class InvoiceLoader
7 {
8 public string Load(string path) => AuditedFile.ReadAllText(path);
9 }
10}
11
12namespace Doc.NamespaceScope.Reporting
13{
14 // The calls in this namespace are not intercepted.
15 public class ReportLoader
16 {
17 public string Load(string path) => File.ReadAllText(path);
18 }
19}
20
Calling the original method
Your method can call the method that it intercepts, before or after its own logic, or not at all. It can also change the arguments, process the result, or handle exceptions.
For instance, the following interceptor calls the intercepted method, then compares its result with a new implementation:
10 public static decimal ComputeTax(
11 LegacyTaxCalculator calculator,
12 decimal amount,
13 string region )
14 {
15 var legacyResult = calculator.ComputeTax( amount, region );
16
17 try
18 {
19 var newResult = NewTaxCalculator.ComputeTax( amount, region );
20
21 if ( newResult != legacyResult )
22 {
23 Console.WriteLine(
24 $"Mismatch for {amount} in {region}: legacy {legacyResult}, new {newResult}." );
25 }
26 }
27 catch ( Exception e )
28 {
29 Console.WriteLine( $"The new calculator failed for {amount} in {region}: {e.Message}" );
30 }
31
32 return legacyResult;
33 }If your method is in the scope of the registration, the call to the intercepted method inside your method is intercepted too. Your method then calls itself.
To prevent this, add the ExcludeInterceptorsAttribute attribute to the class of your interceptor. The calls in a class with this attribute are never intercepted. The following example and the audit example below use this attribute. In the namespace example above, the interceptor is outside the scope of the namespace fabric, so it does not need the attribute.
9[ExcludeInterceptors]
10public static class AuditExample: Comparing a new implementation with the legacy one
The following interceptor runs a new tax calculator next to the legacy one, reports the differences, and returns the result of the legacy calculator. The callers don't change, and the behavior of the program does not change either. When you trust the new calculator, you can switch the calls to it. See the example Switching one module at a time in Choosing which calls to intercept.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4using System.Linq;
5
6namespace Doc.CompareTaxCalculators;
7
8internal class Fabric : ProjectFabric
9{
10 public override void AmendProject( IProjectAmender amender )
11 {
12 var interceptor = typeof(TaxComparison).AsINamedType()
13 .Methods.OfName( nameof(TaxComparison.ComputeTax) )
14 .Single();
15
16 amender.InterceptMethods(
17 calls => calls
18 .Type( typeof(LegacyTaxCalculator) )
19 .Methods( nameof(LegacyTaxCalculator.ComputeTax) )
20 .RedirectToExistingMethod( interceptor ) );
21 }
22}
23
1namespace Doc.CompareTaxCalculators;
2
3public class InvoiceService
4{
5 private readonly LegacyTaxCalculator _calculator = new();
6
7 public decimal ComputeTotal( decimal amount, string region )
8 => amount + this._calculator.ComputeTax( amount, region );
9}
10
1namespace Doc.CompareTaxCalculators;
2
3public class InvoiceService
4{
5 private readonly LegacyTaxCalculator _calculator = new();
6
7 public decimal ComputeTotal(decimal amount, string region)
8 => amount + TaxComparison.ComputeTax(this._calculator, amount, region);
9}
10
1using System;
2
3namespace Doc.CompareTaxCalculators;
4
5internal class Program
6{
7 private static void Main()
8 {
9 var service = new InvoiceService();
10
11 foreach ( var region in new[] { "EU", "US", "CH" } )
12 {
13 Console.WriteLine( $"Total in {region}: {service.ComputeTotal( 100, region )}" );
14 }
15 }
16}
17
1namespace Doc.CompareTaxCalculators;
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
Total in EU: 120.0 Total in US: 110.0 Mismatch for 100 in CH: legacy 10.0, new 8.100. Total in CH: 110.0
1using Metalama.Extensions.Interceptors;
2using System;
3
4namespace Doc.CompareTaxCalculators;
5
6[ExcludeInterceptors]
7public static class TaxComparison
8{
10 public static decimal ComputeTax(
11 LegacyTaxCalculator calculator,
12 decimal amount,
13 string region )
14 {
15 var legacyResult = calculator.ComputeTax( amount, region );
16
17 try
18 {
19 var newResult = NewTaxCalculator.ComputeTax( amount, region );
20
21 if ( newResult != legacyResult )
22 {
23 Console.WriteLine(
24 $"Mismatch for {amount} in {region}: legacy {legacyResult}, new {newResult}." );
25 }
26 }
27 catch ( Exception e )
28 {
29 Console.WriteLine( $"The new calculator failed for {amount} in {region}: {e.Message}" );
30 }
31
32 return legacyResult;
33 }
35}
36
Matching the parameters of the intercepted method
Metalama binds each parameter of your method to a value of the call site:
- When the intercepted method has a receiver, the first parameter of your method receives it.
- The other parameters are matched with the parameters of the intercepted method by name, not by position.
- A parameter of your method that has no match can receive a value given by
WithArgs, the caller information requested by an attribute such as[CallerMemberName], or its own default value. - An argument of the call that your method does not receive is dropped. It is still evaluated when its evaluation can have a side effect.
For instance, the following interceptor of LegacyTaxCalculator.ComputeTax( decimal amount, string region ) receives the calculator, then the arguments by name, then caller information:
15 public static decimal ComputeTax(
16 LegacyTaxCalculator calculator,
17 decimal amount,
18 string region,
19 [CallerMemberName] string caller = "",
20 [CallerLineNumber] int line = 0 )The types do not have to be identical: each argument must convert implicitly to the type of your parameter, and the return value of your method must convert implicitly to the return type of the intercepted method. Parameters passed by ref, out or in must keep their reference kind and their type.
These rules have many details, for instance for omitted optional arguments, params arrays, generic methods and struct receivers. See Interceptor binding rules.
Passing additional parameter values
Your method can receive values that the intercepted method does not have.
Passing a constant value
WithArgs gives values by name. Pass an anonymous object, another object whose public properties and fields give the values, or an IReadOnlyDictionary<string, object?>. A value can be a literal, an enumeration value, a Type, which is written as typeof, or a TypedConstant. See Interceptor binding rules.
For instance:
23.WithArgs( new { category = "FileSystem" } ) );Passing the location of the call
A parameter with a caller information attribute, such as [CallerMemberName], [CallerFilePath] or [CallerLineNumber], receives the location of the intercepted call site, not of the call inside your method.
For instance:
14 public static void DeleteFile(
15 string path,
16 string category,
17 [CallerMemberName] string caller = "",
18 [CallerLineNumber] int line = 0 )Example: Auditing calls to File.Delete
The following project fabric redirects every call to File.Delete to an audit method. WithArgs passes a constant category, and the caller information attributes give the name of the calling member and the line of the call.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4using System.IO;
5using System.Linq;
6
7namespace Doc.AuditFileDelete;
8
9internal class Fabric : ProjectFabric
10{
11 public override void AmendProject( IProjectAmender amender )
12 {
13 var interceptor = typeof(Audit).AsINamedType()
14 .Methods.OfName( nameof(Audit.DeleteFile) )
15 .Single();
16
17 amender.InterceptMethods(
18 calls => calls
19 .Type( typeof(File) )
20 .Methods( nameof(File.Delete) )
21 .RedirectToExistingMethod( interceptor )
23 .WithArgs( new { category = "FileSystem" } ) );
25 }
26}
27
1using System.IO;
2
3namespace Doc.AuditFileDelete;
4
5public class ReportCleaner
6{
7 public void DeleteReport( string directory, string name )
8 => File.Delete( Path.Combine( directory, name + ".pdf" ) );
9
10 public void DeleteDraft( string directory, string name )
11 => File.Delete( Path.Combine( directory, name + ".draft" ) );
12}
13
1using System.IO;
2
3namespace Doc.AuditFileDelete;
4
5public class ReportCleaner
6{
7 public void DeleteReport(string directory, string name)
8 => Audit.DeleteFile(path: Path.Combine(directory, name + ".pdf"), category: "FileSystem", caller: "DeleteReport", line: 10);
9
10 public void DeleteDraft(string directory, string name)
11 => Audit.DeleteFile(path: Path.Combine(directory, name + ".draft"), category: "FileSystem", caller: "DeleteDraft", line: 13);
12}
13
1using System.IO;
2
3namespace Doc.AuditFileDelete;
4
5internal class Program
6{
7 private static void Main()
8 {
9 var cleaner = new ReportCleaner();
10 cleaner.DeleteReport( Path.GetTempPath(), "sales-2026" );
11 cleaner.DeleteDraft( Path.GetTempPath(), "sales-2027" );
12 }
13}
14
1using Metalama.Extensions.Interceptors;
2using System;
3using System.IO;
4using System.Runtime.CompilerServices;
5
6namespace Doc.AuditFileDelete;
7
9[ExcludeInterceptors]
10public static class Audit
12{
14 public static void DeleteFile(
15 string path,
16 string category,
17 [CallerMemberName] string caller = "",
18 [CallerLineNumber] int line = 0 )
20 {
21 Console.WriteLine(
22 $"[{category}] {caller} (line {line}) deletes {Path.GetFileName( path )}." );
23
24 File.Delete( path );
25 }
26}
27
[FileSystem] DeleteReport (line 10) deletes sales-2026.pdf. [FileSystem] DeleteDraft (line 13) deletes sales-2027.draft.
Example: Counting the calls to a legacy API
Before replacing a legacy API, you may want to know which of its call sites actually run in production. The following interceptor counts the calls for each calling member and line, and then calls the legacy implementation.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Code;
3using Metalama.Framework.Fabrics;
4using System.Linq;
5
6namespace Doc.CountCalls;
7
8internal class Fabric : ProjectFabric
9{
10 public override void AmendProject( IProjectAmender amender )
11 {
12 var interceptor = typeof(CallCounter).AsINamedType()
13 .Methods.OfName( nameof(CallCounter.ComputeTax) )
14 .Single();
15
16 amender.InterceptMethods(
17 calls => calls
18 .Type( typeof(LegacyTaxCalculator) )
19 .Methods( nameof(LegacyTaxCalculator.ComputeTax) )
20 .RedirectToExistingMethod( interceptor ) );
21 }
22}
23
1namespace Doc.CountCalls;
2
3public class InvoiceService
4{
5 private readonly LegacyTaxCalculator _calculator = new();
6
7 public decimal ComputeTotal( decimal amount, string region )
8 => amount + this._calculator.ComputeTax( amount, region );
9
10 public decimal ComputeRefund( decimal amount, string region )
11 => -(amount + this._calculator.ComputeTax( amount, region ));
12}
13
14public class LegacyTaxCalculator
15{
16 public decimal ComputeTax( decimal amount, string region )
17 => region == "EU" ? amount * 0.2m : amount * 0.1m;
18}
19
1namespace Doc.CountCalls;
2
3public class InvoiceService
4{
5 private readonly LegacyTaxCalculator _calculator = new();
6
7 public decimal ComputeTotal(decimal amount, string region)
8 => amount + CallCounter.ComputeTax(this._calculator, amount, region, caller: "ComputeTotal", line: 10);
9
10 public decimal ComputeRefund(decimal amount, string region)
11 => -(amount + CallCounter.ComputeTax(this._calculator, amount, region, caller: "ComputeRefund", line: 13));
12}
13
14public class LegacyTaxCalculator
15{
16 public decimal ComputeTax(decimal amount, string region)
17 => region == "EU" ? amount * 0.2m : amount * 0.1m;
18}
19
1namespace Doc.CountCalls;
2
3internal class Program
4{
5 private static void Main()
6 {
7 var service = new InvoiceService();
8
9 for ( var i = 0; i < 3; i++ )
10 {
11 service.ComputeTotal( 100, "EU" );
12 }
13
14 CallCounter.Print();
15 }
16}
17
1using Metalama.Extensions.Interceptors;
2using System;
3using System.Collections.Concurrent;
4using System.Linq;
5using System.Runtime.CompilerServices;
6
7namespace Doc.CountCalls;
8
9[ExcludeInterceptors]
10public static class CallCounter
11{
12 private static readonly ConcurrentDictionary<string, int> _counts = new();
13
15 public static decimal ComputeTax(
16 LegacyTaxCalculator calculator,
17 decimal amount,
18 string region,
19 [CallerMemberName] string caller = "",
20 [CallerLineNumber] int line = 0 )
22 {
23 _counts.AddOrUpdate( $"{caller}, line {line}", 1, ( _, count ) => count + 1 );
24
25 return calculator.ComputeTax( amount, region );
26 }
27
28 public static void Print()
29 {
30 foreach ( var (location, count) in _counts.OrderBy( p => p.Key ) )
31 {
32 Console.WriteLine( $"{location}: {count} call(s)" );
33 }
34 }
35}
36
ComputeTotal, line 10: 3 call(s)
Passing a parameter of the calling method
To pass a value that depends on the call site, decide the interception for each call site with ForEachSite. The delegate receives the call site, which describes the calling code through its Caller property:
- Parameters gives the parameters of the calling member, which you can pass to your method.
- This gives the calling object, when CanAccessThis is
true.
The values passed this way are written at the call site.
For instance, the following delegate passes the requestId parameter of the calling method, when the call site can read it:
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 } ) );The following one passes the calling object, or null in a static member:
23.WithArgs(
24 new
25 {
26 sender = site.Caller.CanAccessThis ? ExpressionFactory.This() : null
27 } ) ) );Example: Passing a parameter of the calling method
The following aspect passes the requestId parameter of the calling method to the interceptor. It skips the call sites where the calling method has no such parameter, and the call sites in lambdas that cannot read it.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.PassCallerParameter;
8
9public class AddRequestIdAttribute : TypeAspect
10{
11 public override void BuildAspect( IAspectBuilder<INamedType> builder )
12 {
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 } ) );
42 }
43}
44
45public static class AuditLogWithRequest
46{
47 public static void Write( string message, string requestId )
48 => AuditLog.Write( $"[{requestId}] {message}" );
49}
50
1using System;
2using System.Linq;
3
4namespace Doc.PassCallerParameter;
5
6[AddRequestId]
7public class OrderController
8{
9 // The calling method has a requestId parameter, which is passed to the interceptor.
10 public void Get( string requestId, int orderId )
11 => AuditLog.Write( $"Reading order {orderId}." );
12
13 // The call is in a lambda, which can also read requestId.
14 public void GetMany( string requestId, int[] orderIds )
15 => orderIds.ToList().ForEach( id => AuditLog.Write( $"Reading order {id}." ) );
16
17 // The calling method has no requestId parameter, so the call is not intercepted.
18 public void Ping() => AuditLog.Write( "Ping." );
19}
20
21public static class AuditLog
22{
23 public static void Write( string message ) => Console.WriteLine( message );
24}
25
26internal class Program
27{
28 private static void Main()
29 {
30 var controller = new OrderController();
31 controller.Get( "r1", 42 );
32 controller.GetMany( "r2", [1, 2] );
33 controller.Ping();
34 }
35}
36
1using System;
2using System.Linq;
3
4namespace Doc.PassCallerParameter;
5
6[AddRequestId]
7public class OrderController
8{
9 // The calling method has a requestId parameter, which is passed to the interceptor.
10 public void Get(string requestId, int orderId)
11 => AuditLogWithRequest.Write(message: $"Reading order {orderId}.", requestId: requestId);
12
13 // The call is in a lambda, which can also read requestId.
14 public void GetMany(string requestId, int[] orderIds)
15 => orderIds.ToList().ForEach(id => AuditLogWithRequest.Write(message: $"Reading order {id}.", requestId: requestId));
16
17 // The calling method has no requestId parameter, so the call is not intercepted.
18 public void Ping() => AuditLog.Write("Ping.");
19}
20
21public static class AuditLog
22{
23 public static void Write(string message) => Console.WriteLine(message);
24}
25
26internal class Program
27{
28 private static void Main()
29 {
30 var controller = new OrderController();
31 controller.Get("r1", 42);
32 controller.GetMany("r2", [1, 2]);
33 controller.Ping();
34 }
35}
36
[r1] Reading order 42. [r2] Reading order 1. [r2] Reading order 2. Ping.
Example: Passing the calling object
The following fabric redirects the calls to Telemetry.Track to a method that also receives the calling object. In a static member, there is no calling object, so null is passed.
1using Metalama.Extensions.Interceptors;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using Metalama.Framework.Fabrics;
5using System.Linq;
6
7namespace Doc.PassCallerInstance;
8
9internal class Fabric : ProjectFabric
10{
11 public override void AmendProject( IProjectAmender amender )
12 => amender.InterceptMethods(
13 calls => calls
14 .Type( typeof(Telemetry) )
15 .Methods( nameof(Telemetry.Track) )
16 .ForEachSite(
17 site => site
18 .RedirectToExistingMethod(
19 typeof(TelemetryWithSender).AsINamedType()
20 .Methods.OfName( nameof(TelemetryWithSender.Track) )
21 .Single() )
23 .WithArgs(
24 new
25 {
26 sender = site.Caller.CanAccessThis ? ExpressionFactory.This() : null
27 } ) ) );
29}
30
1using System;
2
3namespace Doc.PassCallerInstance;
4
5public class OrderService
6{
7 public void PlaceOrder( string product ) => Telemetry.Track( $"Order placed: {product}" );
8
9 public static void Reset() => Telemetry.Track( "Orders reset" );
10}
11
12public static class Telemetry
13{
14 public static void Track( string eventName ) => Console.WriteLine( eventName );
15}
16
1using System;
2
3namespace Doc.PassCallerInstance;
4
5public class OrderService
6{
7 public void PlaceOrder(string product) => TelemetryWithSender.Track(eventName: $"Order placed: {product}", sender: this);
8
9 public static void Reset() => TelemetryWithSender.Track(eventName: "Orders reset", sender: default);
10}
11
12public static class Telemetry
13{
14 public static void Track(string eventName) => Console.WriteLine(eventName);
15}
16
1namespace Doc.PassCallerInstance;
2
3internal class Program
4{
5 private static void Main()
6 {
7 new OrderService().PlaceOrder( "Laptop" );
8 OrderService.Reset();
9 }
10}
11
Order placed: Laptop (sender: OrderService) Orders reset (sender: static code)
1using Metalama.Extensions.Interceptors;
2
3namespace Doc.PassCallerInstance;
4
5[ExcludeInterceptors]
6public static class TelemetryWithSender
7{
8 public static void Track( string eventName, object? sender )
9 => Telemetry.Track( $"{eventName} (sender: {sender?.GetType().Name ?? "static code"})" );
10}
11