Ordering aspects
When several aspect classes are defined, their order of execution is essential.
Concepts
Per-project ordering
In Metalama, the order of execution is static. It is principally a concern of the aspect library author, not a concern of the users of the aspect library.
Each aspect library should define the order of execution of aspects it defines, not only with regards to other aspects of the same library but also to aspects defined in referenced aspect libraries.
When a project uses two unrelated aspect libraries or contains aspect classes, it has to define the ordering in the project itself.
Order of application versus order of execution
Metalama follows the "matryoshka" model: your source code is the innermost doll, and aspects are added around it. The fully compiled code, with all aspects, is like the fully assembled matryoshka. Executing a method is like disassembling the matryoshka: you start with the outermost shell, and you continue to the original implementation.
It is important to remember that Metalama builds the matryoshka from the inside to the outside, but the code is executed from the outside to the inside; 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
Aspects must be ordered using the AspectOrderAttribute assembly-level custom attribute. The order of the aspect classes in the attribute corresponds to their order of execution.
using Metalama.Framework.Aspects;
[assembly: AspectOrder( typeof(Aspect1), typeof(Aspect2), typeof(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))]
This is like in 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 aspect library that defines a conflicting ordering, 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.
Example
The following code snippet shows 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 order of execution is defined as Aspect1 < Aspect2
. You can see from this example 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.
using Doc.Ordering;
using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using System;
using System.Linq;
[assembly: AspectOrder( typeof(Aspect1), typeof(Aspect2) )]
namespace Doc.Ordering
{
internal class Aspect1 : TypeAspect
{
public override void BuildAspect( IAspectBuilder<INamedType> builder )
{
foreach ( var m in builder.Target.Methods )
{
builder.Advice.Override( m, nameof(this.Override) );
}
}
[Introduce]
public static void IntroducedMethod1()
{
Console.WriteLine( "Method introduced by Aspect1." );
}
[Template]
private dynamic? Override()
{
Console.WriteLine(
$"Executing Aspect1 on {meta.Target.Method.Name}. Methods present before applying Aspect1: "
+ string.Join( ", ", meta.Target.Type.Methods.Select( m => m.Name ).ToArray() ) );
return meta.Proceed();
}
}
internal class Aspect2 : TypeAspect
{
public override void BuildAspect( IAspectBuilder<INamedType> builder )
{
foreach ( var m in builder.Target.Methods )
{
builder.Advice.Override( m, nameof(this.Override) );
}
}
[Introduce]
public static void IntroducedMethod2()
{
Console.WriteLine( "Method introduced by Aspect2." );
}
[Template]
private dynamic? Override()
{
Console.WriteLine(
$"Executing Aspect2 on {meta.Target.Method.Name}. Methods present before applying Aspect2: "
+ string.Join( ", ", meta.Target.Type.Methods.Select( m => m.Name ).ToArray() ) );
return meta.Proceed();
}
}
}
using System;
namespace Doc.Ordering
{
[Aspect1]
[Aspect2]
internal class Foo
{
public static void SourceMethod()
{
Console.WriteLine( "Method defined in source code." );
}
}
public static class Program
{
public static void Main()
{
Console.WriteLine( "Executing SourceMethod:" );
Foo.SourceMethod();
Console.WriteLine( "---" );
Console.WriteLine( "Executing IntroducedMethod1:" );
Error CS0117: 'Foo' does not contain a definition for 'IntroducedMethod1'
Foo.IntroducedMethod1();
Console.WriteLine( "---" );
Console.WriteLine( "Executing IntroducedMethod2:" );
Error CS0117: 'Foo' does not contain a definition for 'IntroducedMethod2'
Foo.IntroducedMethod2();
}
}
}
using System;
namespace Doc.Ordering
{
[Aspect1]
[Aspect2]
internal class Foo
{
public static void SourceMethod()
{
Console.WriteLine("Executing Aspect1 on SourceMethod. Methods present before applying Aspect1: SourceMethod, IntroducedMethod2");
Console.WriteLine("Executing Aspect2 on SourceMethod. Methods present before applying Aspect2: SourceMethod");
Console.WriteLine("Method defined in source code.");
return;
}
public static void IntroducedMethod1()
{
Console.WriteLine("Method introduced by Aspect1.");
}
public static void IntroducedMethod2()
{
Console.WriteLine("Executing Aspect1 on IntroducedMethod2. Methods present before applying Aspect1: SourceMethod, IntroducedMethod2");
Console.WriteLine("Method introduced by Aspect2.");
return;
}
}
public static class Program
{
public static void Main()
{
Console.WriteLine("Executing SourceMethod:");
Foo.SourceMethod();
Console.WriteLine("---");
Console.WriteLine("Executing IntroducedMethod1:");
Foo.IntroducedMethod1();
Console.WriteLine("---");
Console.WriteLine("Executing IntroducedMethod2:");
Foo.IntroducedMethod2();
}
}
}
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 several instances of the same aspect type are added to the same declaration, a single instance of the aspect, named the primary instance, is selected and applied to the target. The other instances, named secondary instances, are exposed on the IAspectInstance.SecondaryInstances property, which you can access from meta.AspectInstance or builder.AspectInstance. It is the responsibility of the aspect implementation to decide what to do with the secondary aspect instances.
The primary aspect instance is the instance that has been applied to the "closest" to the target declaration. The sorting criteria are the following:
- Aspects defined using a custom attribute.
- Aspects added by another aspect (child aspects).
- Aspects inherited from another declaration.
- Aspects added by a fabric.
Within these individual categories, the ordering is currently undefined, which means the build may be nondeterministic if the aspect implementation relies on that ordering.