MetalamaConceptual documentationCreating aspectsOrdering aspects
Open sandboxFocusImprove this doc

Ordering aspects

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 constructs the matryoshka from the inside out, the code is executed from the outside in; in other words, the source code is executed last.

Therefore, the aspect application order and the aspect execution order are opposite.

Specifying the execution order

By default, the 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.

using Metalama.Framework.Aspects;
[assembly: AspectOrder( typeof(Aspect1), typeof(Aspect2), typeof(Aspect3))]

This custom attribute defines the following relationships:

flowchart LR

Aspect1 --> Aspect2
Aspect2 --> Aspect3

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 one:

using Metalama.Framework.Aspects;
[assembly: AspectOrder( typeof(Aspect1), typeof(Aspect2))]
[assembly: AspectOrder( 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.

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, Metalama falls back to alphabetical ordering.

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.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7[assembly: AspectOrder( typeof(Aspect1), typeof(Aspect2) )]
8
9namespace Doc.Ordering
10{
11    internal class Aspect1 : TypeAspect
12    {
13        public override void BuildAspect( IAspectBuilder<INamedType> builder )
14        {
15            foreach ( var m in builder.Target.Methods )
16            {
17                builder.Advice.Override( m, nameof(this.Override) );
18            }
19        }
20
21        [Introduce]
22        public static void IntroducedMethod1()
23        {
24            Console.WriteLine( "Method introduced by Aspect1." );
25        }
26
27        [Template]
28        private dynamic? Override()
29        {
30            Console.WriteLine(
31                $"Executing Aspect1 on {meta.Target.Method.Name}. Methods present before applying Aspect1: "
32                + string.Join( ", ", meta.Target.Type.Methods.Select( m => m.Name ).ToArray() ) );
33
34            return meta.Proceed();
35        }
36    }
37
38    internal class Aspect2 : TypeAspect
39    {
40        public override void BuildAspect( IAspectBuilder<INamedType> builder )
41        {
42            foreach ( var m in builder.Target.Methods )
43            {
44                builder.Advice.Override( m, nameof(this.Override) );
45            }
46        }
47
48        [Introduce]
49        public static void IntroducedMethod2()
50        {
51            Console.WriteLine( "Method introduced by Aspect2." );
52        }
53
54        [Template]
55        private dynamic? Override()
56        {
57            Console.WriteLine(
58                $"Executing Aspect2 on {meta.Target.Method.Name}. Methods present before applying Aspect2: "
59                + string.Join( ", ", meta.Target.Type.Methods.Select( m => m.Name ).ToArray() ) );
60
61            return meta.Proceed();
62        }
63    }
64}
Source Code
1using System;
2
3namespace Doc.Ordering
4{
5    [Aspect1]
6    [Aspect2]
7    internal class Foo
8    {
9        public static void SourceMethod()
10        {
11            Console.WriteLine( "Method defined in source code." );
12        }
13    }















14
15    public 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    }
31}
Transformed Code
1using System;
2
3namespace Doc.Ordering
4{
5    [Aspect1]
6    [Aspect2]
7    internal 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            return;
15        }
16
17        public static void IntroducedMethod1()
18        {
19            Console.WriteLine("Method introduced by Aspect1.");
20        }
21
22        public static void IntroducedMethod2()
23        {
24            Console.WriteLine("Executing Aspect1 on IntroducedMethod2. Methods present before applying Aspect1: SourceMethod, IntroducedMethod2");
25            Console.WriteLine("Method introduced by Aspect2.");
26            return;
27        }
28    }
29
30    public static class Program
31    {
32        public static void Main()
33        {
34            Console.WriteLine("Executing SourceMethod:");
35            Foo.SourceMethod();
36
37            Console.WriteLine("---");
38            Console.WriteLine("Executing IntroducedMethod1:");
39            Foo.IntroducedMethod1();
40
41            Console.WriteLine("---");
42            Console.WriteLine("Executing IntroducedMethod2:");
43            Foo.IntroducedMethod2();
44        }
45    }
46}
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.