Project "Caravela" 0.3 / / Caravela Documentation / Conceptual Documentation / Creating Aspects / Design of Caravela Aspect Framework

Design of Caravela Aspect Framework

Overview

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

Aspects have different abilities listed in this article. The aspect author can use or configure these abilities in the following methods inherited from the IAspect<T> interface:

classDiagram class IAspect { BuildAspectClass(IAspectClassBuilder) BuildAspect(IAspectBuilder) } class IAspectBuilder { SkipAspect() TargetDeclaration AdviceFactory } class IAspectClassBuilder { DisplayName Description Layers } class IAspectDependencyBuilder { RequireAspect() } class IAdviceFactory { OverrideMethod(...) IntroduceMethod(...) OverrideFieldOrProperty(...) IntroduceFieldOrProperty(...) } class IDiagnosticSink { Report(...) Suppress(...) } IAspect --> IAspectBuilder : BuildAspect() receives IAspect --> IAspectClassBuilder : BuildAspectClass() receives IAspectBuilder --> IAdviceFactory : exposes IAspectBuilder --> IDiagnosticSink : exposes IAspectClassBuilder --> IAspectDependencyBuilder : exposes

Abilities of aspects

Transforming the target code

Aspects can transform the target code by providing one or many advices. Advices are primitive transformations of code. Advices are safely composable: several aspects that do not know about each other can add advices to the same declaration.

There are two kinds of advices: declarative and imperative.

Declarative advices

The only declarative advice is the member introduction advice and is marked by the IntroduceAttribute custom attribute. For each member of the aspect class annotated with [Introduce], the aspect framework will attempt to introduce the member in the target class. For details, see Introducing Members.

Imperative advices

Imperative advices are added by the implementation of the BuildAspect method thanks to the methods exposed by the AdviceFactory property of the builder parameter. See IAdviceFactory for a complete list of methods. In short:

Template methods

With most advices, you have to provide a template of the member that you want to add to the target type (whether a new member or a new implementation of an existing one).

Templates are made of standard C# code but mix two kinds of code: compile-time and run-time. When an advice is added to some target code, the compile-time part of the corresponding template is executed and what results is the run-time code, which is then added to the source code.

For details, see Writing code templates.

Reporting and suppressing diagnostics

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

For details about this feature, see Reporting and suppressing diagnostics.

Disabling itself

If an aspect instance decides that it cannot be applied to the target it has been applied to, its implementation of the BuildAspect method can call the SkipAspect() method. The effect of this method is to prevent the aspect to provide any advice or child aspect and to set the IsSkipped to true.

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

Specifying on which declarations the aspect can be applied (eligibility)

This feature is not yet implemented.

Validating the final code

This feature is not yet implemented.

Adding child aspects

This feature is not yet implemented.

Requiring other aspects

This feature is not yet implemented.

Defining the name and description of the aspect class in the IDE

To define the appearance of the aspect in the IDE, implement the BuildAspectClass method and set the DisplayName and Description properties of the IAspectClassBuilder.

Warning

Do not reference instance class members in your implementation of BuildAspectClass. Indeed, this method is called on an instance obtained using FormatterServices.GetUninitializedObject -- that is, without invoking the class constructor.

Using several layers of advices

This feature is not yet implemented.

Examples

Example: the OverrideMethodAspect class

Now that you know more about the design of the aspect framework, you can look at the implementation of the OverrideMethodAspect abstract class. You can see that all this class does is providing an abstract method OverrideMethod and adding an advice to the target method where the template is the OverrideMethod.

            [AttributeUsage(AttributeTargets.Method)]
public abstract class OverrideMethodAspect : Attribute, IAspect<IMethod>
{
    public virtual void BuildAspect(IAspectBuilder<IMethod> builder)
    {
        builder.AdviceFactory.OverrideMethod(builder.Target, nameof(this.OverrideMethod));
    }

    public virtual void BuildEligibility(IEligibilityBuilder<IMethod> builder)
    {
        builder.ExceptForInheritance().MustBeNonAbstract();
    }

    public virtual void BuildAspectClass(IAspectClassBuilder builder) { }

    [Template]
    public abstract dynamic? OverrideMethod();
}

          

Example: an aspect targeting methods, fields and properties

The following example shows an aspect that targets methods, fields and properties with a single implementation class.

                using System;
using Caravela.Framework.Aspects;
using Caravela.Framework.Code;

namespace Caravela.Documentation.SampleCode.AspectFramework.LogMethodAndProperty
{
    public class LogAttribute : Attribute, IAspect<IMethod>, IAspect<IFieldOrProperty>
    {
        public void BuildAspect(IAspectBuilder<IMethod> builder)
        {
            builder.AdviceFactory.OverrideMethod(builder.Target, nameof(this.OverrideMethod));
        }

        public void BuildAspect(IAspectBuilder<IFieldOrProperty> builder)
        {
            builder.AdviceFactory.OverrideFieldOrProperty(builder.Target, nameof(this.OverrideProperty));
        }

        [Template]
        private dynamic? OverrideMethod()
        {
            Console.WriteLine("Entering " + meta.Target.Method.ToDisplayString());
            try
            {
                return meta.Proceed();
            }
            finally
            {
                Console.WriteLine(" Leaving " + meta.Target.Method.ToDisplayString());
            }
        }

        [Template]
        private dynamic? OverrideProperty
        {
            get => meta.Proceed();

            set
            {
                Console.WriteLine("Assigning " + meta.Target.Method.ToDisplayString());
                meta.Proceed();
            }
        }
    }
}

              
                namespace Caravela.Documentation.SampleCode.AspectFramework.LogMethodAndProperty
{
    internal class TargetCode
    {
        [Log]
        public int Method(int a, int b)
        {
            return a + b;
        }

        [Log]
        public int Property { get; set; }

        [Log]
        public string Field { get; set; }
    }
}

              
                using System;

namespace Caravela.Documentation.SampleCode.AspectFramework.LogMethodAndProperty
{
    internal class TargetCode
    {
        [Log]
        public int Method(int a, int b)
        {
            Console.WriteLine("Entering TargetCode.Method(int, int)");
            try
            {
                return a + b;
            }
            finally
            {
                Console.WriteLine(" Leaving TargetCode.Method(int, int)");
            }
        }


        private int _property;

        [Log]
        public int Property
        {
            get
            {
                return this._property;
            }

            set
            {
                Console.WriteLine("Assigning TargetCode.Property.set");
                this._property = value;
            }
        }


        private string _field;

        [Log]
        public string Field
        {
            get
            {
                return this._field;
            }

            set
            {
                Console.WriteLine("Assigning TargetCode.Field.set");
                this._field = value;
            }
        }
    }
}
              

Code model versioning

Each aspect, and even each aspect layer, potentially sees a different version of the Caravela.Framework.Code code model. Therefore, if an aspect introduces a member into a type, the next aspects will see that new member in the code model, and will be able to advise it.

Every declaration in the compilation is assigned a depth level. Within the same aspect layer, declarations are processed by increasing order of depth, i.e. base classes are visited before derived classes, and types before their members, and so on.

An aspect, within one depth level, will see the modifications performed by the same aspect on declarations of lower depths.

Aspects cannot modify declarations of lower depth than the target of the aspect.

The Caravela Pipeline

Step 1. Initialization

  1. Generation of the compile-time compilation:

    1. Referenced compile-time projects are identified and loaded.
    2. Compile-time code is identified in the current compilation and a separate compile-time compilation is created.
      1. Templates are transformed into code generating Roslyn syntax trees.
      2. Expressions nameof and typeof are transformed to make them independent from run-time references.
  2. Initialization of aspect classes.

    1. A prototype instance of each aspect class is created using FormatterServices.GetUninitializedObject.
    2. All BuildAspectClass methods are executed. Aspect layers are discovered.
    3. Aspect ordering relationships are discovered in the current project and all referenced assemblies.
    4. Aspects layers are ordered.

Step 2. Applying aspects

For each aspect layer, by order of application (i.e., inverse order of execution, see Ordering Aspects):

  • For the default aspect layer:

    • Aspect sources are evaluated for this aspect type, resulting in a set of target declarations.
    • Target declarations are visited in breadth-first order of depth level, as defined above. For each target declaration:
      • The aspect is instantiated.
      • BuildAspect is invoked.
      • Advices are added to the next steps of the pipeline.
  • For all aspect layers, and for all target declarations visited in breadth-first order:

    • Advices are executed. Advices can provide observable or non-observable transformations (or both), defined as follows:
      • observable transformations are transformations that affect declarations, i.e. they are visible from the code model or from the source code (for instance: introducing a method);
      • non-observable transformations are transformations that only affect the implementation of declarations (for instance: overriding a method).
  • Before we execute the next aspect layer or the next visiting depth, a new code model version is created incrementally from the previous version, including any observable transformation added by an advice before.

Step 3. Transforming the compilation

Before this step, the algorithm collected transformations, but the compilation was never modified.

What happens next depends on whether the pipeline runs at design time or compile time.

Compile time

  1. All transformations (observable and non-observable) are introduced into a new compilation. Templates are expanded at this moment.
  2. The code is linked together and inlined where possible.

Design time

At design time, non-observable transformations are ignored, and partial files are created for observable transformations. Templates are never executed at design time.