MetalamaConceptual documentationCreating aspectsAdvising codeAdding initializers
Open sandboxFocusImprove this doc

Adding initializers

Initialization of fields and properties

Inline initialization of declarative advice

A simple way to initialize a field or property introduced by an aspect is to add an initializer to the template. For instance, if your aspects introduce a field int f and you wish to initialize it to 1, you would write:

[Introduce]
int f = 1;

Example: introducing a Guid property

In the example below, the aspect introduces an Id property of type Guid and initializes it to a new unique value.

1using Metalama.Framework.Aspects;
2using System;
3
4namespace Doc.IntroduceId
5{
6    internal class IntroduceIdAttribute : TypeAspect
7    {
8        [Introduce]
9        public Guid Id { get; } = Guid.NewGuid();
10    }
11}
Source Code
1namespace Doc.IntroduceId
2{


3    [IntroduceId]
4    internal class MyClass { }
5}
Transformed Code
1using System;
2
3namespace Doc.IntroduceId
4{
5    [IntroduceId]
6    internal class MyClass
7    {
8        public Guid Id { get; } = Guid.NewGuid();
9    }
10}

Example: initializing with a template

The T# template language can also be used inside analyzers of fields or properties. The aspect in the following example introduces a property that is initialized to the build configuration and target framework.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Fabrics;
3
4namespace Doc.BuildInfo
5{
6    internal partial class BuildInfo
7    {
8        private class Fabric : TypeFabric
9        {
10            [Introduce]
11            public string? TargetFramework { get; } = meta.Target.Project.TargetFramework;
12
13            [Introduce]
14            public string? Configuration { get; } = meta.Target.Project.Configuration;
15        }
16    }
17}
Source Code
1namespace Doc.BuildInfo
2{

3    internal partial class BuildInfo { }
4}
Transformed Code
1namespace Doc.BuildInfo
2{
3
4#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
5
6    internal partial class BuildInfo
7    {
8        public string? Configuration { get; } = "Debug";
9
10
11        public string? TargetFramework { get; } = "net6.0";
12    }
13
14#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
15
16
17}

Initialization of programmatic advice

If you use the programmatic advice IntroduceProperty, IntroduceField or IntroduceEvent, you can set the InitializerExpression in the lambda passed to the build* parameter of these advice methods.

Example: initializing a programmatically introduced field

In the following example, the aspect introduces a field using the IntroduceField programmatic advice and sets its initializer expression to an array that contains the name of all methods in the target type.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using System.Linq;
5
6namespace Doc.ProgrammaticInitializer
7{
8    internal class AddMethodNamesAspect : TypeAspect
9    {
10        public override void BuildAspect( IAspectBuilder<INamedType> builder )
11        {
12            // Create an expression that contains the array with all method names.
13            var arrayBuilder = new ArrayBuilder( typeof(string) );
14
15            foreach ( var methodName in builder.Target.Methods.Select( m => m.Name ).Distinct() )
16            {
17                arrayBuilder.Add( ExpressionFactory.Literal( methodName ) );
18            }
19
20            // Introduce a field and initialize it to that array.
21            builder.Advice.IntroduceField(
22                builder.Target,
23                "_methodNames",
24                typeof(string[]),
25                buildField: f => f.InitializerExpression = arrayBuilder.ToExpression() );
26        }
27    }
28}
Source Code
1namespace Doc.ProgrammaticInitializer
2{

3    [AddMethodNamesAspect]
4    internal class Foo
5    {
6        private void M1() { }
7
8        private void M2() { }
9    }
10}
Transformed Code
1namespace Doc.ProgrammaticInitializer
2{
3    [AddMethodNamesAspect]
4    internal class Foo
5    {
6        private void M1() { }
7
8        private void M2() { }
9
10        private string[] _methodNames = new string[] {
11            "M1",
12            "M2"
13        };
14    }
15}

Before any object constructor

To inject some initialization before any user code of the instance constructor is called:

  1. Add a method of signature void BeforeInstanceConstructor() to your aspect class and annotate it with the [Template] custom attribute. The name of this method is arbitrary.
  2. Call the builder.Advice.AddInitializer method in your aspect (or amender.Advice.AddInitializer in a fabric). Pass the type that must be initialized, then the name of the method from the previous step, and finally the value InitializerType.BeforeInstanceConstructor.

The AddInitializer advice will not affect the constructors that call a chained this constructor. That is, the advice always runs before any constructor of the current class. However, the initialization logic runs after the call to the base constructor if the advised constructor calls the base constructor.

A default constructor will be created automatically if the type does not contain any constructor.

Example: registering live instances

The following aspect registers any new instance of the target class in a registry of live instances. After an instance has been garbage-collected, it is automatically removed from the registry. The aspect injects the registration logic into the constructor of the target class.

1using System;
2using System.Collections.Concurrent;
3using System.Collections.Generic;
4using System.Threading;
5
6namespace Doc.RegisterInstance
7{
8    internal static class Program
9    {
10        private static void Main()
11        {
12            Console.WriteLine( "Allocate object." );
13            AllocateObject();
14
15            Console.WriteLine( "GC.Collect()" );
16            GC.Collect();
17
18            PrintInstances();
19        }
20
21        private static void AllocateObject()
22        {
23            var o = new DemoClass();
24
25            PrintInstances();
26
27            _ = o;
28        }
29
30        private static void PrintInstances()
31        {
32            foreach ( var instance in InstanceRegistry.GetInstances() )
33            {
34                Console.WriteLine( instance );
35            }
36        }
37    }
38
39    public static class InstanceRegistry
40    {
41        private static int _nextId;
42        private static readonly ConcurrentDictionary<int, WeakReference<object>> _instances = new();
43
44        public static IDisposable Register( object instance )
45        {
46            var id = Interlocked.Increment( ref _nextId );
47            _instances.TryAdd( id, new WeakReference<object>( instance ) );
48
49            return new Handle( id );
50        }
51
52        private static void Unregister( int id )
53        {
54            _instances.TryRemove( id, out _ );
55        }
56
57        public static IEnumerable<object> GetInstances()
58        {
59            foreach ( var weakReference in _instances.Values )
60            {
61                if ( weakReference.TryGetTarget( out var instance ) )
62                {
63                    yield return instance;
64                }
65            }
66        }
67
68        private class Handle : IDisposable
69        {
70            private int _id;
71
72            public Handle( int id )
73            {
74                this._id = id;
75            }
76
77            public void Dispose()
78            {
79                GC.SuppressFinalize( this );
80                Unregister( this._id );
81            }
82
83            ~Handle()
84            {
85                Unregister( this._id );
86            }
87        }
88    }
89}
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.RegisterInstance
7{
8    public class RegisterInstanceAttribute : TypeAspect
9    {
10        [Introduce]
        Warning CS8618: Non-nullable field '_instanceRegistryHandle' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

11        private IDisposable _instanceRegistryHandle;
12
13        public override void BuildAspect( IAspectBuilder<INamedType> builder )
14        {
15            base.BuildAspect( builder );
16
17            builder.Advice.AddInitializer( builder.Target, nameof(this.BeforeInstanceConstructor), InitializerKind.BeforeInstanceConstructor );
18        }
19
20        [Template]
21        private void BeforeInstanceConstructor()
22        {
23            this._instanceRegistryHandle = InstanceRegistry.Register( meta.This );
24        }
25    }
26}
Source Code
1namespace Doc.RegisterInstance
2{


3    [RegisterInstance]
4    internal class DemoClass
5    {
6        public DemoClass() : base() { }
7


8        public DemoClass( int i ) : this() { }



9
10        public DemoClass( string s ) { }



11    }
12}
Transformed Code
1using System;
2
3namespace Doc.RegisterInstance
4{
5    [RegisterInstance]
6    internal class DemoClass
7    {
8        public DemoClass() : base()
9        {
10            this._instanceRegistryHandle = InstanceRegistry.Register(this);
11        }
12
13        public DemoClass(int i) : this() { }
14
15        public DemoClass(string s)
16        {
17            this._instanceRegistryHandle = InstanceRegistry.Register(this);
18        }
19
20        private IDisposable _instanceRegistryHandle;
21    }
22}
Allocate object.
Doc.RegisterInstance.DemoClass
GC.Collect()

Before a specific object constructor

If you wish to insert logic into a specific constructor, call the AddInitializer method and pass an IConstructor. With this method overload, you can advise the constructors chained to another constructor of the same type through the this keyword.

Before the type constructor

The same approach can be used to add logic to the type constructor (i.e., static constructor) instead of the object constructor. In this case, the InitializerType.BeforeTypeConstructor value should be used.