Open sandboxFocusImprove this doc

Aspect composition

Aspect composition occurs when multiple aspects are applied to the same class. Metalama addresses this complexity through a consistent and deterministic model for aspect composition.

Consider three critical points:

  1. Strong ordering of aspects and advice
  2. Code model versioning
  3. Safe composition of advice

Strong ordering of aspects and advice

Aspects are entities that take a code model as input and produce outputs such as advice, diagnostics, validators, and child aspects. The only output relevant to this discussion is advice, as other outputs don't alter the code. While most aspects have a single layer of advice, you can define multiple layers.

To ensure a consistent order of execution for aspects and advice, Metalama employs two ordering criteria:

  1. Aspect layer: The aspect author or user must specify the order of execution for aspect layers. For more information about aspect layer ordering, see Ordering aspects.

  2. Depth level of target declarations: Metalama assigns every declaration in the compilation a depth level. Within the same aspect layer, declarations are processed in order of increasing depth. For example, base classes are visited before derived classes, and types are processed before their members.

Metalama executes aspects and advice in the same layer, applied to declarations of the same depth, in an unspecified order and potentially concurrently on multiple threads.

Code model versioning

The code model represents declarations but excludes implementations such as method bodies or initializers. Therefore, the only types of advice that affect the code model are introductions and interface implementations. Overriding an existing method doesn't impact the code model because it only changes the implementation.

For each aspect layer and depth level, Metalama creates a new version of the code model that reflects the changes made by the previous aspect layer or depth level.

If an aspect introduces a member into a type, subsequent aspects will see that new member in the code model and can advise it.

To maintain the consistency of this model, aspects can't provide outputs to previous aspects or to declarations that aren't below the current target.

Safe composition of advice

When several aspects add advice to the same declaration, Metalama ensures that the resulting code will be correct.

For example, if two aspects override the same method, both aspects are guaranteed to compose correctly. Metalama resolves this complexity automatically.

Example: Log and cache

This example shows a Log aspect and a Cache aspect applied to the same method. The [assembly: AspectOrder] attribute specifies that Log runs before Cache at run time, making logging the outermost layer:

  • Log wraps around everything (entry/exit logged for every call)
  • Cache runs inside logging (cache hits still log entry/exit)
  • The original method runs innermost (only on cache miss)

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System;
4using System.Collections.Concurrent;
5
6namespace Doc.LogAndCache;
7
8// A simple logging aspect that writes to the console.
9public class LogAttribute : OverrideMethodAspect
10{
11    public override dynamic? OverrideMethod()
12    {
13        Console.WriteLine( $"Entering {meta.Target.Method}" );
14
15        try
16        {
17            return meta.Proceed();
18        }
19        finally
20        {
21            Console.WriteLine( $"Leaving {meta.Target.Method}" );
22        }
23    }
24}
25
26// A simple caching aspect that stores results in a dictionary.
27public class CacheAttribute : OverrideMethodAspect
28{
29    [Introduce( WhenExists = OverrideStrategy.Ignore )]
30    private static readonly ConcurrentDictionary<string, object?> _cache = new();
31
32    public override dynamic? OverrideMethod()
33    {
34        // Build a cache key from method name and arguments.
35        var key = $"{meta.Target.Type}.{meta.Target.Method.Name}({string.Join( ", ", meta.Target.Method.Parameters.ToValueArray() )})";
36
37        if ( _cache.TryGetValue( key, out var cached ) )
38        {
39            Console.WriteLine( $"Cache hit for {key}" );
40
41            return cached;
42        }
43
44        Console.WriteLine( $"Cache miss for {key}" );
45        var result = meta.Proceed();
46        _cache[key] = result;
47
48        return result;
49    }
50}
51
Source Code
1using Metalama.Framework.Aspects;
2using System;
3
4// Specify that Log runs before Cache, so logging wraps around caching.

5[assembly: AspectOrder( AspectOrderDirection.RunTime, typeof(Doc.LogAndCache.LogAttribute), typeof(Doc.LogAndCache.CacheAttribute) )]
6
7namespace Doc.LogAndCache;
8
9public partial class PriceCalculator
10{
11    // Both [Log] and [Cache] are applied to the same method.
12    // AspectOrder ensures Log executes first at run time (outermost).
13    [Log]
14    [Cache]
15    public decimal GetPrice( string productId )
16    {
17        Console.WriteLine( $"  Computing price for {productId}..." );
18






19        return productId.Length * 10m;






20    }
21}







22
Transformed Code
1using Metalama.Framework.Aspects;
2using System;
3using System.Collections.Concurrent;
4
5// Specify that Log runs before Cache, so logging wraps around caching.
6[assembly: AspectOrder(AspectOrderDirection.RunTime, typeof(Doc.LogAndCache.LogAttribute), typeof(Doc.LogAndCache.CacheAttribute))]
7
8namespace Doc.LogAndCache;
9
10public partial class PriceCalculator
11{
12    // Both [Log] and [Cache] are applied to the same method.
13    // AspectOrder ensures Log executes first at run time (outermost).
14    [Log]
15    [Cache]
16    public decimal GetPrice(string productId)
17    {
18        Console.WriteLine("Entering PriceCalculator.GetPrice(string)");
19        try
20        {
21            var key = $"PriceCalculator.GetPrice({string.Join(", ", new object[] { productId })})";
22            if (_cache.TryGetValue(key, out var cached))
23            {
24                Console.WriteLine($"Cache hit for {key}");
25                return (decimal)cached;
26            }
27
28            Console.WriteLine($"Cache miss for {key}");
29            decimal result;
30            Console.WriteLine($"  Computing price for {productId}...");
31
32            result = productId.Length * 10m;
33            _cache[key] = result;
34            return result;
35        }
36        finally
37        {
38            Console.WriteLine("Leaving PriceCalculator.GetPrice(string)");
39        }
40    }
41
42    private static readonly ConcurrentDictionary<string, object?> _cache = new();
43}
44