When multiple aspect classes are defined, the execution order becomes critical.
Concepts
Per-project ordering
Note
Defining the execution order is the primary responsibility of the aspect library author, not the users of the aspect library. Aspect libraries that know about each other should set their execution order properly to avoid user confusion.
Each aspect library should define the execution order of the aspects it introduces. This order should consider not only other aspects within the same library but also aspects defined in referenced aspect libraries.
When a project employs two unrelated aspect libraries or contains aspect classes, it must define the ordering within the project itself.
Order of application versus order of execution
Metalama adheres to the "matryoshka" model: your source code is the innermost doll, and aspects are added around it. The fully compiled code, inclusive of all aspects, resembles a fully assembled matryoshka. Executing a method is akin to disassembling the matryoshka: you commence with the outermost shell and progress to the original implementation.
It's crucial to remember that while Metalama, at build time, constructs the matryoshka from the inside out, at run time the code is executed from the outside in; in other words, the source code is executed last.
Therefore, the build-time order of applying aspects order and the run-time order of executing aspects are usually opposite.
Specifying the execution order
By default, the run-time execution order of aspects is alphabetical. This order is not intended to be correct but at least it is deterministic.
The execution order of aspects must be defined using the AspectOrderAttribute assembly-level custom attribute. The order of the aspect classes in the attribute corresponds to their execution order. To avoid ambiguities, you must explicitly supply the AspectOrderDirection value (RunTime
or CompileTime
) for which you are specifying the aspect order.
The two following snippets are equivalent:
using Metalama.Framework.Aspects;
[assembly: AspectOrder( AspectOrderDirection.RunTime, typeof(Aspect1), typeof(Aspect2), typeof(Aspect3))]
using Metalama.Framework.Aspects;
[assembly: AspectOrder( AspectOrderDirection.CompileTime, typeof(Aspect3), typeof(Aspect2), typeof(Aspect1))]
This custom attribute defines the run-time execution order:
flowchart LR Aspect1 --> Aspect2 Aspect2 --> Aspect3
Partial relationships
You can specify partial order relationships. The aspect framework will merge all partial relationships and determine the global order for the current project.
For instance, the following code snippet is equivalent to the previous ones:
using Metalama.Framework.Aspects;
[assembly: AspectOrder( AspectOrderDirection.RunTime, typeof(Aspect1), typeof(Aspect2))]
[assembly: AspectOrder( AspectOrderDirection.RunTime, typeof(Aspect2), typeof(Aspect3))]
These two attributes define the following relationships:
This is akin to mathematics: if we have a < b
and b < c
, then we have a < c
, and the ordered sequence is {a, b, c}
.
If you specify conflicting relationships or import an aspect library that defines a conflicting order, Metalama will emit a compilation error.
Note
Metalama will merge all [assembly: AspectOrder(...)]
attributes that it finds not only in the current project but also in all referenced projects or libraries. Therefore, you don't need to repeat the [assembly: AspectOrder(...)]
attributes in all projects that use aspects. It is sufficient to define them in projects that define aspects.
Inherited aspects
By default, relationships specified with AspectOrderAttribute also apply to derived aspect classes.
For instance, consider the following aspects:
abstract class ExceptionHandlingAspect;
class RetryAspect : ExceptionHandlingAspect;
class WrapExceptionAspect : ExceptionHandlingAspect;
abstract class CacheAspect;
class B1 : MemoryCacheAspect;
class B2 : RedisCacheAspect;
Consider the following order attributes, ordering only abstract aspects:
using Metalama.Framework.Aspects;
[assembly: AspectOrder( AspectOrderDirection.RunTime, typeof(CacheAspect), typeof(ExceptionHandlingAspect))]
We don't explicitly order concrete aspect classes, so alphabetical ordering will automatically apply.
The resulting run-time aspect order will be the following:
flowchart LR MemoryCacheAspect --> RedisCacheAspect --> RetryAspect --> WrapExceptionAspect
To disable this behavior, set the ApplyToDerivedTypes property to false
.
How does it work?
Under the hood, Metalama performs a topological sort on a graph composed of all relationships found in the current project and all its dependencies.
When a pair of aspects do not have any specific ordering relationship, from any source, Metalama falls back to alphabetical ordering to avoid any non-determinism.
Example
The following code snippet demonstrates two aspects that add a method to the target type and display the list of methods defined on the target type before the aspect was applied. The execution order is defined as Aspect1 < Aspect2
. From this example, you can discern that the order of application of aspects is opposite. Aspect2
is applied first and sees the source code, then Aspect1
is applied and sees the method added by Aspect1
. The modified method body of SourceMethod
shows that the aspects are executed in this order: Aspect1
, Aspect2
, then the original method.
1using Doc.Ordering;
2using Metalama.Framework.Advising;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Code;
5using System;
6using System.Linq;
7
8[assembly: AspectOrder( AspectOrderDirection.RunTime, typeof(Aspect1), typeof(Aspect2) )]
9
10namespace Doc.Ordering;
11
12internal class Aspect1 : TypeAspect
13{
14 public override void BuildAspect( IAspectBuilder<INamedType> builder )
15 {
16 foreach ( var m in builder.Target.Methods )
17 {
18 builder.With( m ).Override( nameof(this.Override) );
19 }
20 }
21
22 [Introduce]
23 public static void IntroducedMethod1()
24 {
25 Console.WriteLine( "Method introduced by Aspect1." );
26 }
27
28 [Template]
29 private dynamic? Override()
30 {
31 Console.WriteLine(
32 $"Executing Aspect1 on {meta.Target.Method.Name}. Methods present before applying Aspect1: "
33 + string.Join( ", ", meta.Target.Type.Methods.Select( m => m.Name ).ToArray() ) );
34
35 return meta.Proceed();
36 }
37}
38
39internal class Aspect2 : TypeAspect
40{
41 public override void BuildAspect( IAspectBuilder<INamedType> builder )
42 {
43 foreach ( var m in builder.Target.Methods )
44 {
45 builder.With( m ).Override( nameof(this.Override) );
46 }
47 }
48
49 [Introduce]
50 public static void IntroducedMethod2()
51 {
52 Console.WriteLine( "Method introduced by Aspect2." );
53 }
54
55 [Template]
56 private dynamic? Override()
57 {
58 Console.WriteLine(
59 $"Executing Aspect2 on {meta.Target.Method.Name}. Methods present before applying Aspect2: "
60 + string.Join( ", ", meta.Target.Type.Methods.Select( m => m.Name ).ToArray() ) );
61
62 return meta.Proceed();
63 }
64}
1using System;
2
3namespace Doc.Ordering;
4
5[Aspect1]
6[Aspect2]
7internal class Foo
8{
9 public static void SourceMethod()
10 {
11 Console.WriteLine( "Method defined in source code." );
12 }
13}
14
15public static class Program
16{
17 public static void Main()
18 {
19 Console.WriteLine( "Executing SourceMethod:" );
20 Foo.SourceMethod();
21
22 Console.WriteLine( "---" );
23 Console.WriteLine( "Executing IntroducedMethod1:" );
Error CS0117: 'Foo' does not contain a definition for 'IntroducedMethod1'
24 Foo.IntroducedMethod1();
25
26 Console.WriteLine( "---" );
27 Console.WriteLine( "Executing IntroducedMethod2:" );
Error CS0117: 'Foo' does not contain a definition for 'IntroducedMethod2'
28 Foo.IntroducedMethod2();
29 }
30}
1using System;
2
3namespace Doc.Ordering;
4
5[Aspect1]
6[Aspect2]
7internal class Foo
8{
9 public static void SourceMethod()
10 {
11 Console.WriteLine("Executing Aspect1 on SourceMethod. Methods present before applying Aspect1: SourceMethod, IntroducedMethod2");
12 Console.WriteLine("Executing Aspect2 on SourceMethod. Methods present before applying Aspect2: SourceMethod");
13 Console.WriteLine("Method defined in source code.");
14 }
15
16 public static void IntroducedMethod1()
17 {
18 Console.WriteLine("Method introduced by Aspect1.");
19 }
20
21 public static void IntroducedMethod2()
22 {
23 Console.WriteLine("Executing Aspect1 on IntroducedMethod2. Methods present before applying Aspect1: SourceMethod, IntroducedMethod2");
24 Console.WriteLine("Method introduced by Aspect2.");
25 }
26}
27
28public static class Program
29{
30 public static void Main()
31 {
32 Console.WriteLine("Executing SourceMethod:");
33 Foo.SourceMethod();
34
35 Console.WriteLine("---");
36 Console.WriteLine("Executing IntroducedMethod1:");
37 Foo.IntroducedMethod1();
38
39 Console.WriteLine("---");
40 Console.WriteLine("Executing IntroducedMethod2:");
41 Foo.IntroducedMethod2();
42 }
43}
Executing SourceMethod: Executing Aspect1 on SourceMethod. Methods present before applying Aspect1: SourceMethod, IntroducedMethod2 Executing Aspect2 on SourceMethod. Methods present before applying Aspect2: SourceMethod Method defined in source code. --- Executing IntroducedMethod1: Method introduced by Aspect1. --- Executing IntroducedMethod2: Executing Aspect1 on IntroducedMethod2. Methods present before applying Aspect1: SourceMethod, IntroducedMethod2 Method introduced by Aspect2.
Several instances of the same aspect type on the same declaration
When multiple instances of the same aspect type are applied to the same declaration, one instance of the aspect, known as the primary instance, is selected and applied to the target. The other instances, known as secondary instances, are exposed on the IAspectInstance.SecondaryInstances property, which you can access from meta.AspectInstance or builder.AspectInstance. The aspect implementation is responsible for determining what to do with the secondary aspect instances.
The primary aspect instance is the instance that has been applied closest to the target declaration. The sorting criteria are as follows: 1. Aspects defined using a custom attribute. 2. Aspects added by another aspect (child aspects). 3. Aspects inherited from another declaration. 4. Aspects added by a fabric.
Within these individual categories, the ordering is currently undefined, meaning the build may be nondeterministic if the aspect implementation relies on that ordering.