Metalama 1.0 / / Metalama Documentation / Conceptual Documentation / Creating Aspects / Transforming Code / Adding Initializers

Adding Initializers

Initialization of fields and properties

Inline initialization of declarative advice

The simple way to initialize a field or property introduced by an aspect is to add an initializer to the template. That is, if your aspects introduce a field int f and you want to initialize it to 1, simply write:

            [Introduce] 
int f = 1;

          

Example: introducing a Guid property

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

using Metalama.Framework.Aspects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Doc.IntroduceId
{
    internal class IntroduceIdAttribute : TypeAspect
    {
        [Introduce]
        public Guid Id { get; } = Guid.NewGuid();
    }
}
namespace Doc.IntroduceId
{
    [IntroduceId]
    class MyClass 
    {

    }
}
using System;

namespace Doc.IntroduceId
{
    [IntroduceId]
    class MyClass
    {


        private Guid _id = Guid.NewGuid();

        public Guid Id
        {
            get
            {
                return this._id;
            }
        }
    }
}

Example: initializing with a template

You can also use the T# template language inside field and property analyzers. The aspect in the following example introduces a property that is initialized to the build configuration and target framework.

using Metalama.Framework.Aspects;
using Metalama.Framework.Fabrics;

namespace Doc.BuildInfo
{
    internal partial class BuildInfo
    {
        private class Fabric : TypeFabric
        {
            [Introduce]
            public string? TargetFramework { get; } = meta.Target.Project.TargetFramework;

            [Introduce]
            public string? Configuration { get; } = meta.Target.Project.Configuration;
        }

    }
}
namespace Doc.BuildInfo
{
    internal partial class BuildInfo
    {
      

    }
}
namespace Doc.BuildInfo
{
#pragma warning disable CS0067
    internal partial class BuildInfo
    {


        private string? _configuration = "Debug";

        public string? Configuration
        {
            get
            {
                return this._configuration;
            }
        }

        private string? _targetFramework = "net6.0";

        public string? TargetFramework
        {
            get
            {
                return this._targetFramework;
            }
        }

    }
#pragma warning restore CS0067
}

Initialization of programmatic advice

If you use the programmatic advice IntroduceProperty, IntroduceField or IntroduceEvent you can set the Metalama.Framework.Code.DeclarationBuilders.IFieldOrPropertyBuilder.InitializerExpression of the builder object that these advice methods return, and set it to any expression.

Example: initializing a programmatically introduced field

The example in the following 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.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Code.SyntaxBuilders;
using System.Linq;

namespace Doc.ProgrammaticInitializer
{
    internal class AddMethodNamesAspect : TypeAspect
    {
        public override void BuildAspect( IAspectBuilder<INamedType> builder )
        {
            // Create an expression that contains the an array with all method names.
            var expressionBuilder = new ExpressionBuilder();
            expressionBuilder.AppendVerbatim( "new string[] {" );
            var i = 0;
            foreach ( var methodName in builder.Target.Methods.Select( m => m.Name ).Distinct() )
            {
                if ( i > 0 )
                {
                    expressionBuilder.AppendVerbatim( ", " );
                }

                expressionBuilder.AppendLiteral( methodName );

                i++;
            }
            expressionBuilder.AppendVerbatim( "}" );

            // Introduce a field and initialize it to that array.
            var fieldBuilder = builder.Advice.IntroduceField( builder.Target, "_methodNames" );
            fieldBuilder.Type = TypeFactory.GetType( typeof( string[] ) );
            fieldBuilder.InitializerExpression = expressionBuilder.ToExpression();
        }
    }
}
namespace Doc.ProgrammaticInitializer
{
    [AddMethodNamesAspect]
   internal class Foo
    {
        private void M1() { }
        private void M2() { }

    }
}
namespace Doc.ProgrammaticInitializer
{
    [AddMethodNamesAspect]
    internal class Foo
    {
        private void M1() { }
        private void M2() { }

        private string[] _methodNames = new string[] { "M1", "M2" };
    }
}

Before 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 of 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 does run after the call to the base constructor, if any.

If the type does not contain any constructor, a default constructor will be created.

Example: registering live instances

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

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using System;

namespace Doc.RegisterInstance
{
    public class RegisterInstanceAttribute : TypeAspect
    {
        [Introduce]
        private IDisposable _instanceRegistryHandle;

        public override void BuildAspect( IAspectBuilder<INamedType> builder )
        {
            base.BuildAspect( builder );

            builder.Advice.AddInitializer( builder.Target, nameof( BeforeInstanceConstructor ), InitializerKind.BeforeInstanceConstructor );
        }

        [Template]
        private void BeforeInstanceConstructor()
        {
            this._instanceRegistryHandle = InstanceRegistry.Register( meta.This );
        }
    }

}
namespace Doc.RegisterInstance
{

    [RegisterInstance]
    internal class DemoClass
    {
    }

}
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;

namespace Doc.RegisterInstance
{

    internal static class Program
    {
        private static void Main()
        {
            Console.WriteLine( "Allocate object." );
            AllocateObject();

            Console.WriteLine( "GC.Collect()" );                
            GC.Collect();

            PrintInstances();
        }

        private static void AllocateObject()
        {
            var o = new DemoClass();

            PrintInstances();
        }

        private static void PrintInstances()
        {
            foreach ( var instance in InstanceRegistry.GetInstances() )
            {
                Console.WriteLine( instance );
            }
        }
    }

    public static class InstanceRegistry
    {
        private static int _nextId;
        private static readonly ConcurrentDictionary<int, WeakReference<object>> _instances = new();

        public static IDisposable Register( object instance )
        {
            var id = Interlocked.Increment( ref _nextId );
            _instances.TryAdd( id, new WeakReference<object>( instance ) );

            return new Handle( id );
        }

        private static void Unregister( int id )
        {
            _instances.TryRemove( id, out _ );
        }

        public static IEnumerable<object> GetInstances()
        {
            foreach ( var weakReference in _instances.Values )
            {
                if ( weakReference.TryGetTarget( out var instance ) )
                {
                    yield return instance;
                }
            }
        }

        private class Handle : IDisposable
        {
            private int _id;

            public Handle( int id )
            {
                this._id = id;
            }

            public void Dispose()
            {
                GC.SuppressFinalize( this );
                Unregister( _id );
            }

            ~Handle()
            {
                Unregister( _id );
            }
        }

    }





}
// Warning CS8618 on `_instanceRegistryHandle`: `Non-nullable field '_instanceRegistryHandle' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.`
using System;

namespace Doc.RegisterInstance
{

    [RegisterInstance]
    internal class DemoClass
    {
        private IDisposable _instanceRegistryHandle;

        public DemoClass()
        {
            this._instanceRegistryHandle = InstanceRegistry.Register(this);
        }
    }

}

Before type constructor

The same approach as above can be used to add logic to the type constructor (i.e. static constructor) instead of the object constructor. The InitializerType.BeforeTypeConstructor value needs to be used instead.