Open sandboxFocusImprove this doc

Understanding the aspect framework design

Until now, you've learned how to create simple aspects using the OverrideMethodAspect and OverrideFieldOrPropertyAspect. These classes can be viewed as API sugar, designed to simplify the creation of your first aspects. Before going deeper, you need to understand the design of the Metalama aspect framework.

Class diagram

By definition, an aspect is a class that implements the IAspect<T> generic interface. The generic parameter of this interface represents the type of declarations to which the aspect can be applied. For instance, an aspect applicable to a method must implement the IAspect<IMethod> interface, while an aspect applicable to a named type must implement IAspect<INamedType>.

The aspect author can utilize the BuildAspect method, inherited from the IAspect<T> interface, to construct the aspect instance applied to a specific target declaration, using an IAspectBuilder<TAspectTarget>.

classDiagram

    class IAdviser {
        Target
        With(declaration)
    }

    class AdviserExtensions {
        <<static>>
        Override(...)
        Introduce*(...)
        ImplementInterface(...)
        AddContract(...)
        AddInitializer(...)
    }

    class IAspect {
        BuildAspect(IAspectBuilder)
        BuildEligibility(IEligibilityBuilder)
    }

    class IAspectInstance {
        Aspect
        TargetDeclaration
        AspectState
    }

    class IAspectState {
        <<interface>>
    }

    class IAspectBuilder {
        SkipAspect()
        TargetDeclaration
        AspectState
    }

    class ScopedDiagnosticSink {
        Report(...)
        Suppress(...)
        Suggest(...)
    }

    class IQuery {
        Select(...)
        SelectMany(...)
        SelectTypes(...)
        Where(...)
    }

    class AspectQueryExtensions {
        <<static>>
        AddAspect(...)
        AddAspectIfEligible(...)
        RequireAspect(...)
    }

    AdviserExtensions --> IAdviser : provides extension\nmethods
    AspectQueryExtensions --> IQuery : provides extension\nmethods
    IAspect "1" --> "*" IAspectInstance : instantiated as
    IAspectInstance --> IAspectState : stores
    IAspect --> IAspectBuilder : BuildAspect() receives
    IAspectBuilder --|> IAdviser : inherits
    IAspectBuilder --> ScopedDiagnosticSink : exposes
    IAspectBuilder --> IQuery : exposes

Design principles

BuildAspect is the entry point

The BuildAspect method is called once for each target declaration to which the aspect is applied. This method receives an IAspectBuilder<TAspectTarget>, which provides access to the target declaration and exposes methods to add advice (code transformations), report or suppress diagnostics, and perform other tasks.

Aspects are both compile-time and run-time objects

Aspects can optionally be custom attributes (by deriving from Attribute). In this case, aspect classes exist both at compile time (when Metalama executes them) and at run time (as standard .NET attributes accessible via reflection).

This dual nature has important implications:

  • Aspect properties set in source code (e.g., [Log(Category = "Security")]) are available both to BuildAspect at compile time and to reflection at run time.
  • Run-time code can query aspects using standard reflection APIs like GetCustomAttributes.

Aspects are serializable

All aspects are serializable. However, serialization is only used for inheritable aspects in cross-project scenarios. For details, see Serialization of aspects and other compile-time classes.

Aspects must be immutable

Aspects must be designed as immutable classes. Never store state in aspect fields from the BuildAspect method if that state depends on the target declaration.

Aspect instances are not necessarily associated with a single target declaration. When aspects are inherited or added through fabrics, the same IAspect instance is shared by many target declarations. The BuildAspect method is called many times, once per target. If you store target-specific state in a field, that state will be shared across all targets, leading to incorrect behavior.

Target-specific data is represented by IAspectInstance, which pairs an IAspect with a specific target declaration. If you need to store target-specific state that must be accessible to other aspects or validators, use IAspectState via the AspectState property. For details, see Sharing state with advice.

For strategies to pass state from BuildAspect to templates without storing it in aspect fields, see Sharing state with advice.

Abilities of aspects

1. Transforming code

Aspects can perform the following transformations to code:

  • Apply a template to an existing method, i.e., add generated code to user-written code.
  • Introduce a newly generated member to an existing type.
  • Implement an interface into a type.

For more details, refer to Transforming code.

2. Reporting, suppressing diagnostics, and suggesting code fixes

Aspects can report diagnostics (a term encompassing errors, warnings, and information messages) and suppress diagnostics reported by the C# compiler, analyzers, or other aspects.

Aspects can suggest code fixes for any diagnostic they report or propose code refactorings.

For more information about this feature, refer to Reporting and suppressing diagnostics.

3. Performing advanced code validations

The builder.Outbound property allows registering validators for advanced scenarios:

  • Validate the target declaration after it has been transformed by all aspects.
  • Validate any references to the target declaration.

Refer to Validating code from an aspect.

4. Adding other aspects to be applied

The builder.Outbound property also allows adding other aspects to the target code.

Refer to Adding child aspects.

5. Defining its eligibility

Aspects can define which declarations they can be legally applied to.

Refer to Defining the eligibility of aspects.

6. Disabling itself

If an aspect instance decides it can't be applied to its target, its implementation of the BuildAspect method can call the SkipAspect() method. This method prevents the aspect from providing any advice or child aspect and sets the IsSkipped to true.

The aspect may or may not report a diagnostic before calling SkipAspect(). Calling this method doesn't report any diagnostic.

7. Customizing its appearance in the IDE

By default, an aspect class is represented in the IDE by the class name trimmed of its Attribute suffix, if any. To override the default name, annotate the aspect class with the DisplayNameAttribute annotation.

Examples

Example: an aspect targeting methods, fields, and properties

The following example demonstrates an aspect that targets methods, fields, and properties with a single implementation class. The aspect implements IAspect<IMethod>, IAspect<IFieldOrProperty>, and uses the BuildAspect method to add logging behavior to each target type.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System;
4
5namespace Doc.LogMethodAndProperty;
6
7[AttributeUsage( AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property )]
8public class LogAttribute : Aspect, IAspect<IMethod>, IAspect<IFieldOrProperty>
9{
10    public void BuildAspect( IAspectBuilder<IMethod> builder )
11    {
12        builder.Override( nameof(this.OverrideMethod) );
13    }
14
15    public void BuildAspect( IAspectBuilder<IFieldOrProperty> builder )
16    {
17        builder.Override( nameof(this.OverrideFieldOrProperty) );
18    }
19
20    [Template]
21    private dynamic? OverrideMethod()
22    {
23        Console.WriteLine( "Entering " + meta.Target.Method.ToDisplayString() );
24
25        try
26        {
27            return meta.Proceed();
28        }
29        finally
30        {
31            Console.WriteLine( " Leaving " + meta.Target.Method.ToDisplayString() );
32        }
33    }
34
35    [Template]
36    private dynamic? OverrideFieldOrProperty
37    {
38        get => meta.Proceed();
39
40        set
41        {
42            Console.WriteLine( "Assigning " + meta.Target.FieldOrProperty.ToDisplayString() );
43            meta.Proceed();
44        }
45    }
46}
Source Code
1namespace Doc.LogMethodAndProperty;
2


3internal class Foo
4{
5    [Log]
6    public int Method( int a, int b )
7    {
8        return a + b;
9    }



10
11    [Log]





12    public int Property { get; set; }


13
14    [Log]














15    public string? Field;
16}
Transformed Code
1using System;
2
3namespace Doc.LogMethodAndProperty;
4
5internal class Foo
6{
7    [Log]
8    public int Method(int a, int b)
9    {
10        Console.WriteLine("Entering Foo.Method(int, int)");
11        try
12        {
13            return a + b;
14        }
15        finally
16        {
17            Console.WriteLine(" Leaving Foo.Method(int, int)");
18        }
19    }
20
21    private int _property;
22
23    [Log]
24    public int Property
25    {
26        get
27        {
28            return _property;
29        }
30
31        set
32        {
33            Console.WriteLine("Assigning Foo.Property");
34            _property = value;
35        }
36    }
37
38    private string? _field;
39
40    [Log]
41    public string? Field
42    {
43        get
44        {
45            return _field;
46        }
47
48        set
49        {
50            Console.WriteLine("Assigning Foo.Field");
51            _field = value;
52        }
53    }
54}