Open sandboxFocusImprove this doc

Decoupling Aspects From Attributes

When reading other articles in this documentation, you may have come under the impression that an aspect is necessarily a custom attribute. This is not the case. Aspects and attributes are different concepts. You can build aspects that do not derive from the Attribute class.

The reason why most aspects are derived from the Attribute class is convenience and simplicity. Indeed, as you can see from their source code, classes like ConstructorAspect, EventAspect, FieldAspect, FieldOrPropertyAspect, MethodAspect, ParameterAspect, PropertyAspect, TypeAspect, or TypeParameterAspect are only API sugar. They are all implementations of the IAspect<T> interface that derive from Attribute.

If you want to build an aspect that must not be represented as an attribute, you can implement a class that implements IAspect<T> but not Attribute. You can then add this aspect using a fabric (see Adding many aspects simultaneously) or a child aspect (see Adding child aspects).

However, what if you still want the aspect to be added using a custom attribute, but you don't want the custom attribute to implement the IAspect<T> interface? This is what we will see in this article.

Why would you want to decouple aspects from attributes?

Decoupling an aspect from its attribute means that we are decoupling the aspect implementation from its "interface" or contract. This aspect contract is the custom attribute. The implementation is any class implementing the IAspect<T> interface.

This has several benefits:

  • The custom attribute can reside in a package for which you don't have the source code (for instance, those of the System.ComponentModel.DataAnnotations namespace) or where you cannot or don't want to add a reference to Metalama.
  • You can prepare your code for an easier departure from Metalama, either by switching to another code generation or validation technology or by using our metalama divorce feature. See Divorcing from Metalama for details.
  • You can have different implementations of the aspect in different projects or namespaces.

Decoupling when the contract project can have a reference to Metalama

We will first look at the situation where the project that contains the custom attribute can reference the Metalama.Framework project. This approach is simpler because the custom attribute can be constructed into a compile-time object.

Step 1. Create the custom attribute class

  1. Make sure the project has a reference to the Metalama.Framework package.
  2. Create the class that will become the aspect contract. Derive it from Attribute and include its constructor parameters and properties.
  3. Add two custom attributes to this class:
  4. Add the ICompileTimeSerializable interface to the type. This is an empty interface, but it will instruct Metalama to generate a serializer for your attribute. This step is only useful if the aspect is expected to have cross-project effects, for instance aspect inheritance or adding reference validation.

Step 2. Create the aspect implementation class

This step can be performed in a separate project, which must also have a reference to the Metalama.Framework package.

  1. Create a class that implements the IAspect<T> interface where T is any kind of declaration to which the attribute can be added. For instance, if your attribute can be applied to methods and properties, implement IAspect<T> for both IMethod and IProperty.

  2. Add a constructor that accepts the attribute object and stores it in an instance field.

  3. Implement your aspect as usual. The only difference is that the attribute object is not the aspect object itself but is available on the instance field.

Step 3. Bind the custom attribute to the aspect using a fabric

The last step is to add an aspect for each instance of the custom attribute. This is typically done in a fabric.

  1. Create a ProjectFabric (or use an existing one).
  2. Use the SelectDeclarationsWithAttribute method to find all declarations that have the attribute created in Step 1.
  3. For declaration types supported by your aspect (i.e., all values of T in IAspect<T>), use OfType to select these declarations.
  4. Call the AddAspect method. Supply a lambda that instantiates the aspect. To get the attribute instance, use the IDeclaration.Attributes collection and then the GetConstructedAttributesOfType method. Pass the instance to the aspect constructor.

Example: decoupled logging aspect with reference to Metalama.Framework

In this example, we show a traditional logging aspect whose API is a custom attribute that does not implement the IAspect<T>. However, the attribute uses the Metalama-specific [RunTimeOrCompileTime] attribute, which simplifies the implementation of the aspect.

The attribute can be applied to both methods and properties. It has a Category property, which the aspect must include in the logged string.

1using Metalama.Framework.Advising;
2using System;
3using System.Linq;
4using Metalama.Framework.Aspects;
5using Metalama.Framework.Code;
6using Metalama.Framework.Fabrics;
7
8namespace Doc.Decoupled;
9
10internal class LogAspect : IAspect<IMethod>, IAspect<IProperty>
11{
12    private readonly LogAttribute _attribute;
13
14    public LogAspect( LogAttribute attribute )
15    {
16        this._attribute = attribute;
17    }
18
19    public void BuildAspect( IAspectBuilder<IMethod> builder )
20    {
21        builder.Override( nameof(this.MethodTemplate) );
22    }
23
24    public void BuildAspect( IAspectBuilder<IProperty> builder )
25    {
26        builder.OverrideAccessors( null, nameof(this.MethodTemplate) );
27    }
28
29    [Template]
30    public dynamic? MethodTemplate()
31    {
32        Console.WriteLine( $"[{this._attribute.Category}] Executing {meta.Target.Method}" );
33
34        return meta.Proceed();
35    }
36}
1using System;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Serialization;
4
5namespace Doc.Decoupled;
6
7[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
8[RunTimeOrCompileTime]
9public class LogAttribute : Attribute, ICompileTimeSerializable
10{
11    public string Category { get; set; } = "default";
12}
1using Metalama.Framework.Advising;
2using System;
3using System.Linq;
4using Metalama.Framework.Aspects;
5using Metalama.Framework.Code;
6using Metalama.Framework.Fabrics;
7
8namespace Doc.Decoupled;
9
10public class Fabric : ProjectFabric
11{
12    public override void AmendProject( IProjectAmender amender )
13    {
14        var declarations = amender.SelectDeclarationsWithAttribute<LogAttribute>();
15
16        declarations
17            .OfType<IMethod>()
18            .AddAspectIfEligible<LogAspect>(
19                m =>
20                {
21                    var attribute = m.Attributes.GetConstructedAttributesOfType<LogAttribute>().Single();
22
23                    return new LogAspect( attribute );
24                } );
25        
26        declarations
27            .OfType<IProperty>()
28            .AddAspectIfEligible<LogAspect>(
29                m =>
30                {
31                    var attribute = m.Attributes.GetConstructedAttributesOfType<LogAttribute>().Single();
32
33                    return new LogAspect( attribute );
34                } );
35    }
36}
37
Source Code
1using EmptyFiles;
2

3namespace Doc.Decoupled;
4
5public class C
6{
7    public void UnmarkedMethod() { }
8
9    [Log( Category = "Foo" )]






10    public void MarkedMethod() { }
11
12    [Log( Category = "Bar" )]
13    public string MarkedProperty { get; set; }
14}
Transformed Code
1using System;
2using EmptyFiles;
3
4namespace Doc.Decoupled;
5
6public class C
7{
8    public void UnmarkedMethod() { }
9
10    [Log(Category = "Foo")]
11    public void MarkedMethod()
12    {
13        Console.WriteLine("[Foo] Executing C.MarkedMethod()");
14    }
15
16    private string _markedProperty = default!;
17
18    [Log(Category = "Bar")]

19    public string MarkedProperty
20    {
21        get
22        {
23            return _markedProperty;
24        }
25
26        set
27        {
28            Console.WriteLine("[Bar] Executing C.MarkedProperty.set");
29            _markedProperty = value;
30        }
31    }
32}

Decoupling when the contract project cannot have a reference to Metalama

If you can't or don't want to add a reference to Metalama.Framework in the contract project, the process is a bit more difficult. The reason is that the custom attribute is a run-time only class, which means that it cannot be constructed at build time. However, the aspect constructor and its BuildAspect method all run at build time. That means that you cannot pass the attribute itself to the aspect.

Instead, your aspect must have a parameter of type IRef<IAttribute>. The IAttribute interface plays a similar role as the System.Reflection.CustomAttributeData class. It exposes attribute data as the ConstructorArguments and NamedArguments properties. However, the IAttribute interface is bound to a specific compilation revision. Aspects should generally not store compilation-specific objects but references. This is why we will store an IRef<T>. References, unlike declarations, are serializable and can therefore be used in cross-project scenarios such as aspect inheritance or cross-project validation.

Step 1. Create the custom attribute class

This step is very similar to the first step of the first approach but we will not have anything Metalama-related.

  1. Create the class that will become the aspect contract. Derive it from Attribute and include its constructor parameters and properties.
  2. Add the [AttributeUsage] custom attribute to this class.

Step 2. Create the aspect implementation class

We now create the aspect itself. The only difference compared to the first approach is that our constructor parameter must be of type IRef<IAttribute>.

  1. Create a class that implements the IAspect<T> interface where T is any kind of declaration to which the attribute can be added. For instance, if your attribute can be applied to methods and properties, implement IAspect<T> for both IMethod and IProperty.

  2. Add a constructor with a parameter of type IRef<IAttribute> and store its value in an instance field of the same type.

  3. Implement your aspect as usual. In any aspect method, to get the IAttribute, use the IRef.GetTarget() method. Then use the ConstructorArguments and NamedArguments properties to get access to attribute data.

Step 3. Bind the custom attribute to the aspect using a fabric

The last step is again to add an aspect for each instance of the custom attribute. The main difference is how we pass the attribute to the aspect.

  1. Create a ProjectFabric (or use an existing one).
  2. Use the SelectDeclarationsWithAttribute method to find all declarations that have the attribute created in Step 1.
  3. For declaration types supported by your aspect (i.e., all values of T in IAspect<T>), use OfType to select these declarations.
  4. Call the AddAspect method. Supply a lambda that instantiates the aspect. To get the attribute data, use the IDeclaration.Attributes collection and then the OfAttributeType method. Use the IAttribute.ToRef() method to get the reference, and pass it to the aspect constructor.

Example: decoupled logging aspect without any references to Metalama.Framework

We revise the previous example and remove any references to Metalama.Framework from the contract attribute. Therefore, the attribute must be handled as an IRef<IAttribute>. As you can see in the aspect implementation, the logic to retrieve the Category is more complex.

1using Metalama.Framework.Advising;
2using System;
3using System.Linq;
4using Metalama.Framework.Aspects;
5using Metalama.Framework.Code;
6using Metalama.Framework.Fabrics;
7
8namespace Doc.Decoupled_Ref;
9
10internal class LogAspect : IAspect<IMethod>, IAspect<IProperty>
11{
12    private readonly IRef<IAttribute> _attribute;
13
14    public LogAspect( IRef<IAttribute> attribute )
15    {
16        this._attribute = attribute;
17    }
18
19    public void BuildAspect( IAspectBuilder<IMethod> builder )
20    {
21        builder.Override( nameof(this.MethodTemplate) );
22    }
23
24    public void BuildAspect( IAspectBuilder<IProperty> builder )
25    {
26        builder.OverrideAccessors( null, nameof(this.MethodTemplate) );
27    }
28
29    [Template]
30    public dynamic? MethodTemplate()
31    {
32        var attribute = this._attribute.GetTarget();
33
34        var category =
35            attribute.GetArgumentValue( nameof(LogAttribute.Category), "default" )!;
36
37        Console.WriteLine( $"[{category}] Executing {meta.Target.Method}" );
38
39        return meta.Proceed();
40    }
41}
1using System.Linq;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Fabrics;
5
6namespace Doc.Decoupled_Ref;
7
8public class Fabric : ProjectFabric
9{
10    public override void AmendProject( IProjectAmender amender )
11    {
12        var declarations = amender.SelectDeclarationsWithAttribute( typeof(LogAttribute));
13
14        declarations
15            .OfType<IMethod>()
16            .AddAspectIfEligible<LogAspect>(
17                m =>
18                {
19                    var attribute = m.Attributes.OfAttributeType( typeof(LogAttribute) )
20                        .Single();
21
22                    return new LogAspect( attribute.ToRef() );
23                } );
24        
25        declarations
26            .OfType<IProperty>()
27            .AddAspectIfEligible<LogAspect>(
28                p =>
29                {
30                    var attribute = p.Attributes.OfAttributeType( typeof(LogAttribute) )
31                        .Single();
32
33                    return new LogAspect( attribute.ToRef() );
34                } );
35    }
36}
Source Code


1namespace Doc.Decoupled_Ref;
2
3public class C
4{
5    public void UnmarkedMethod() { }






6
7    [Log( Category = "Foo" )]
8    public void MarkedMethod() { }
9
10    [Log( Category = "Bar" )]
11    public string MarkedProperty { get; set; }












12}
Transformed Code
1using System;
2
3namespace Doc.Decoupled_Ref;
4
5public class C
6{
7    public void UnmarkedMethod() { }
8
9    [Log(Category = "Foo")]
10    public void MarkedMethod()
11    {
12        Console.WriteLine("[Foo] Executing C.MarkedMethod()");
13    }
14
15    private string _markedProperty = default!;

16
17    [Log(Category = "Bar")]
18    public string MarkedProperty
19    {
20        get
21        {
22            return _markedProperty;
23        }
24
25        set
26        {
27            Console.WriteLine("[Bar] Executing C.MarkedProperty.set");
28            _markedProperty = value;
29        }
30    }
31}
1using System;
2
3namespace Doc.Decoupled_Ref;
4
5[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
6public class LogAttribute : Attribute
7{
8    public string Category { get; set; } = "default";
9}
10