Open sandboxFocusImprove this doc

Sharing state with advice

When you need to share compile-time state between different pieces of advice or between your implementation of the BuildAspect method and the advice, there are several strategies available to you.

Note

This article is about sharing compile-time state. If you need to share run-time state with advice, a different strategy must be adopted. For instance, you could introduce a field in the target type and utilize it from several advice methods.

Warning

DO NOT share state with an aspect field if that state depends on the target declaration of the aspect. In scenarios involving inherited aspects or cross-project validators, the same instance of the aspect class will be reused across all inherited targets. Always design aspects as immutable classes.

Sharing state with compile-time template parameters

This is the most direct approach for passing values from your BuildAspect method to a template method. However, it is only applicable to method, constructor, or accessor templates.

For more details, refer to Template parameters and type parameters.

Sharing state with tags

Compile-time template parameters are not available for event, property, or field templates. A straightforward alternative is to use tags, which are arbitrary name-value pairs.

The idea is to set tags in your BuildAspect method and to read them from the template implementation.

Tags can be represented as arbitrary objects (including anonymously typed objects) or as IReadOnlyDictionary<string, object?> objects. When the tags object does not readily implement the dictionary interface, an accessor implementing the IReadOnlyDictionary<string, object?> interface is created, giving access to the object properties through a dictionary.

For instance, the anonymous object new { A = 5, B = "x", C = builder.Target.DeclaringType } defines three tags arbitrarily named A, B, and C.

There are two ways to add tags from the BuildAspect method:

  • by passing an argument to the tags parameter of the advice method, or
  • by setting the IAspectBuilder.Tags property at any moment in the BuildAspect method (even after the advice method has been called).

When you use both ways at the same time, the tags will be merged into a single dictionary.

In your template implementation, you can read the tags by calling the meta.Tags API, which returns an IObjectReader. This interface derives from IReadOnlyDictionary<string, object?>. For instance, you would use the meta.Tags["A"] expression to access the tag named A that you defined in the previous step.

The IObjectReader interface has an additional property Source that exposes the original object, not flattened as a dictionary.

Example: Tags passed as an argument

In the following example, the tags are set by passing an argument to the advice method. We use an anonymous type to set the value and the dictionary interface to read it.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.Tags;
7
8internal class TagsAspect : MethodAspect
9{
10    public override void BuildAspect( IAspectBuilder<IMethod> builder )
11    {
12        builder.Override(
13            nameof(this.OverrideMethod),
14            tags: new { ParameterCount = builder.Target.Parameters.Count } );
15    }
16
17    [Template]
18    private dynamic? OverrideMethod()
19    {
20        Console.WriteLine( $"This method has {meta.Tags["ParameterCount"]} parameters." );
21
22        return meta.Proceed();
23    }
24}
Source Code
1using System;
2
3namespace Doc.Tags;
4
5internal class Foo
6{
7    [TagsAspect]
8    private void Bar( int a, int b )
9    {
10        Console.WriteLine( $"Method({a}, {b})" );
11    }
12}
Transformed Code
1using System;
2
3namespace Doc.Tags;
4
5internal class Foo
6{
7    [TagsAspect]
8    private void Bar(int a, int b)
9    {
10        Console.WriteLine("This method has 2 parameters.");
11        Console.WriteLine($"Method({a}, {b})");
12    }
13}

Example: Tags set as a property

In the following example, the tags are set by setting the aspectBuilder.Tags property. We defined a compile-time record to represent the tags in a strongly typed way and use the IObjectReader.Source property to read the object.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System;
4
5namespace Doc.Tags_Property;
6
7internal class TagsAspect : TypeAspect
8{
9    [CompileTime]
10    private record Tags( int NumberOfProperties, int NumberOfFields );
11
12    public override void BuildAspect( IAspectBuilder<INamedType> builder )
13    {
14        builder.Tags = new Tags(
15            builder.Target.Properties.Count,
16            builder.Target.Fields.Count );
17    }
18
19    [Introduce]
20    private void PrintInfo()
21    {
22        var tags = (Tags) meta.Tags.Source!;
23
24        Console.WriteLine(
25            $"This method has {tags.NumberOfFields} fields and {tags.NumberOfProperties} properties." );
26    }
27}
Source Code
1using System;
2
3namespace Doc.Tags_Property;
4
5[TagsAspect]
6internal class Foo
7{
8    private int _a, _b;
9
10    public int Sum => this._a + this._b;
11}
Transformed Code
1using System;
2
3namespace Doc.Tags_Property;
4
5[TagsAspect]
6internal class Foo
7{
8    private int _a, _b;
9
10    public int Sum => this._a + this._b;
11
12    private void PrintInfo()
13    {
14        Console.WriteLine("This method has 2 fields and 1 properties.");
15    }
16}

Sharing state with the AspectState property

When you want to share state not from templates inside the current aspect instance, but with other aspect instances, you set the IAspectBuilder.AspectState property. Its value is exposed for read-only access on the IAspectInstance.AspectState property. It is therefore visible to child aspects and aspects that inherit from them (i.e., successors) through the Predecessors property.

This property is opaque to the Metalama framework. You can use it for any purpose but at your own risk. You are responsible for thread safety if you choose to have any mutable state in your aspect state.

Objects assigned to AspectState must implement the IAspectState interface, which makes them automatically serializable. This mechanism ensures that the aspect state is available in cross-project scenarios. For details, see Serialization of aspects and other compile-time classes.

Sharing state with annotations

A last way to share state with successor aspects is to use annotations. Annotations are arbitrary objects attached to declarations. They are visible from every aspect. However, unlike aspect state, annotations are not serialized and are only visible within the current project.

You can add annotations from the BuildAspect method using the AddAnnotation advice method.

You can read annotations using declaration.Enhancements().GetAnnotations<T> where T is the type of your annotation (see GetAnnotations).