Open sandboxFocusImprove this doc

Adding initializers

Initialization of fields and properties

Declarative introductions

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 aspect introduces a field int f and you want 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
6internal class IntroduceIdAttribute : TypeAspect
7{
8    [Introduce]
9    public Guid Id { get; } = Guid.NewGuid();
10}
Source Code
1namespace Doc.IntroduceId;
2


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

Example: Initializing with a template

The T# template language can also be used within initializers for 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
6internal 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}
Source Code
1namespace Doc.BuildInfo;
2

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

Programmatic introductions

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 names 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
8internal 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.IntroduceField(
22            "_methodNames",
23            typeof(string[]),
24            buildField: f => f.InitializerExpression = arrayBuilder.ToExpression() );
25    }
26}
Source Code
1namespace Doc.ProgrammaticInitializer;
2

3[AddMethodNamesAspect]
4internal class Foo
5{
6    private void M1() { }
7
8    private void M2() { }
9}
Transformed Code
1namespace Doc.ProgrammaticInitializer;
2
3[AddMethodNamesAspect]
4internal 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}

Before the type constructor

To inject logic into the type (static) constructor, use InitializerKind.BeforeTypeConstructor. The aspect's template runs once per type (or per generic type instance, in case of generic types) at the first use of that type, after any static field initializers, but before any user code in the existing static constructor, if any.

Example: Self-registering a generic message handler

The following aspect targets a generic Handler<TMessage>. For every closed type the compiler or program constructs, such as Handler<OrderPlaced> and Handler<OrderShipped>, the generated static constructor registers the pair (typeof(TMessage), typeof(Handler<TMessage>)) with a static MessageRouter.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System.Linq;
5
6namespace Doc.BeforeTypeConstructor;
7
8public class RegisterMessageHandlerAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11    {
12        var selfType = builder.Target.MakeGenericInstance(
13            builder.Target.TypeParameters.ToArray<IType>() );
14
15        builder.AddInitializer(
16            nameof(this.RegisterHandler),
17            InitializerKind.BeforeTypeConstructor,
18            args: new
19            {
20                TSelf = selfType,
21                TMessage = builder.Target.TypeParameters[0]
22            } );
23    }
24
25    [Template]
26    private static void RegisterHandler<[CompileTime] TSelf, [CompileTime] TMessage>()
27        where TSelf : new()
28        where TMessage : IMessage
29    {
30        MessageRouter.Register<TSelf, TMessage>();
31    }
32}
33
Source Code
1using System;
2
3namespace Doc.BeforeTypeConstructor;
4
5[RegisterMessageHandler]
6public partial class Handler<TMessage>
7    where TMessage : IMessage
8{
9    public void Handle( TMessage message )
10    {
11        Console.WriteLine( $"Handling {message}." );
12    }





13}
14
Transformed Code
1using System;
2
3namespace Doc.BeforeTypeConstructor;
4
5[RegisterMessageHandler]
6public partial class Handler<TMessage>
7    where TMessage : IMessage
8{
9    public void Handle(TMessage message)
10    {
11        Console.WriteLine($"Handling {message}.");
12    }
13
14    static Handler()
15    {
16        MessageRouter.Register<Handler<TMessage>, TMessage>();
17    }
18}
19
1namespace Doc.BeforeTypeConstructor;
2
3internal class Program
4{
5    private static void Main()
6    {
7        // Touching each closed generic type triggers its static
8        // constructor, which registers it with the router.
9        _ = new Handler<OrderPlaced>();
10        _ = new Handler<OrderShipped>();
11
12        MessageRouter.Dispatch( new OrderPlaced( "O-42" ) );
13        MessageRouter.Dispatch( new OrderShipped( "O-42" ) );
14    }
15}
16
1using System;
2using System.Collections.Generic;
3
4namespace Doc.BeforeTypeConstructor;
5
6public interface IMessage;
7
8public record OrderPlaced( string OrderId ) : IMessage;
9
10public record OrderShipped( string OrderId ) : IMessage;
11
12public static class MessageRouter
13{
14    private static readonly Dictionary<Type, Type> _handlerTypes = new();
15
16    public static void Register<THandler, TMessage>()
17        where THandler : new()
18        where TMessage : IMessage
19    {
20        lock ( _handlerTypes )
21        {
22            _handlerTypes[typeof(TMessage)] = typeof(THandler);
23        }
24    }
25
26    public static void Dispatch( IMessage message )
27    {
28        Type? handlerType;
29
30        lock ( _handlerTypes )
31        {
32            if ( !_handlerTypes.TryGetValue( message.GetType(), out handlerType ) )
33            {
34                Console.WriteLine( $"No handler for {message.GetType().Name}." );
35
36                return;
37            }
38        }
39
40        var handler = Activator.CreateInstance( handlerType )!;
41        var handleMethod = handlerType.GetMethod( "Handle" )!;
42        handleMethod.Invoke( handler, new object[] { message } );
43    }
44}
45
Handling OrderPlaced { OrderId = O-42 }.
Handling OrderShipped { OrderId = O-42 }.

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, the name of the method from the previous step, and the value InitializerKind.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 doesn't contain any constructor.

This initializer kind also supports records, including positional records. The initializer code is injected into the primary 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 Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.RegisterInstance;
7
8public class RegisterInstanceAttribute : TypeAspect
9{
10    [Introduce]
11    private IDisposable _instanceRegistryHandle;
12
13    public override void BuildAspect( IAspectBuilder<INamedType> builder )
14    {
15        base.BuildAspect( builder );
16
17        builder.AddInitializer(
18            nameof(this.BeforeInstanceConstructor),
19            InitializerKind.BeforeInstanceConstructor );
20    }
21
22    [Template]
23    private void BeforeInstanceConstructor()
24    {
25        this._instanceRegistryHandle = InstanceRegistry.Register( meta.This );
26    }
27}
Source Code
1namespace Doc.RegisterInstance;
2


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


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



9
10    public DemoClass( string s ) { }



11}
Transformed Code
1using System;
2
3namespace Doc.RegisterInstance;
4
5[RegisterInstance]
6internal class DemoClass
7{
8    public DemoClass()
9    {
10        _instanceRegistryHandle = InstanceRegistry.Register(this);
11    }
12
13    public DemoClass(int i) : this() { }
14
15    public DemoClass(string s)
16    {
17        _instanceRegistryHandle = InstanceRegistry.Register(this);
18    }
19
20    private IDisposable _instanceRegistryHandle;
21}
1using System;
2using System.Collections.Concurrent;
3using System.Collections.Generic;
4using System.Threading;
5
6namespace Doc.RegisterInstance;
7
8internal 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
39public 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 readonly 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}
Allocate object.
Doc.RegisterInstance.DemoClass
GC.Collect()

Example: Initializing a record

The following example applies BeforeInstanceConstructor to a positional record. The primary constructor is materialized into a normal constructor and a set of properties. The initializer code is injected at the beginning of the synthetised constructor.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6namespace Doc.RecordInitializer;
7
8public class LogConstructionAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11    {
12        builder.AddInitializer(
13            nameof(this.BeforeConstructor),
14            InitializerKind.BeforeInstanceConstructor );
15    }
16
17    [Template]
18    private void BeforeConstructor()
19    {
20        Console.WriteLine( $"Constructing {meta.Target.Type.Name}." );
21    }
22}
23
Source Code
1namespace Doc.RecordInitializer;
2


3[LogConstruction]
4public sealed partial record Product( string Name, decimal Price );
5
Transformed Code
1using System;
2
3namespace Doc.RecordInitializer;
4
5[LogConstruction]
6public sealed partial record Product
7{
8    public string Name { get; init; }
9    public decimal Price { get; init; }
10
11    public void Deconstruct(out string Name, out decimal Price)
12    {
13        Name = this.Name;
14        Price = this.Price;
15    }
16
17    public Product(string Name, decimal Price)
18    {
19        this.Name = Name;
20        this.Price = Price;
21        Console.WriteLine("Constructing Product.");
22    }
23}

Before a specific object constructor

If you want 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.

After the last instance constructor

To inject logic that executes after the whole chain of instance constructors has executed for an object, use InitializerKind.AfterLastInstanceConstructor. This is useful when you need to perform actions after the constructor has fully initialized the object, but before the object initializer or the with expression sets fields and properties.

  1. Add a template method to your aspect class and annotate it with [Template].
  2. Call the AddInitializer method with the value InitializerKind.AfterLastInstanceConstructor.

Metalama introduces an OnConstructed helper method on the target type and emits calls to it from every constructor. Constructors that chain to another constructor of the same type using this(...) still include the generated call, but duplicate execution is prevented by the InitializationContext and its IsHandled check.

For non-sealed types, the introduced method is protected virtual, allowing derived types to participate in the initialization chain. An InitializationContext parameter is added to each constructor to coordinate initialization across inheritance hierarchies.

By default, the aspect's statements are appended to OnConstructed after the call to base.OnConstructed(...), in aspect-application order (i.e. CompileTime order). This can be customized through the InitializerPosition argument of AddInitializer; see Adding initializers for details.

Example: Publishing a domain event after construction

The following aspect publishes a domain event once an object has been fully constructed. If any constructor throws, the event is not published, so subscribers only see successfully-created instances. Because the generated OnConstructed method is protected virtual, a derived type such as RecurringOrder inherits the initialization chain automatically and its constructor also ends with the call to OnConstructed.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4
5namespace Doc.AfterLastInstanceConstructor;
6
7public class PublishWhenCreatedAttribute : TypeAspect
8{
9    public override void BuildAspect( IAspectBuilder<INamedType> builder )
10    {
11        builder.AddInitializer(
12            nameof(this.OnCreated),
13            InitializerKind.AfterLastInstanceConstructor );
14    }
15
16    [Template]
17    private void OnCreated()
18    {
19        DomainEvents.Publish(
20            new EntityCreated( meta.Target.Type.Name, meta.This ) );
21    }
22}
23
Source Code
1using System;
2
3namespace Doc.AfterLastInstanceConstructor;


4
5[PublishWhenCreated]
6public partial class Order
7{
8    public string CustomerId { get; }
9
10    public Order( string customerId )
11    {
12        this.CustomerId = customerId;
13    }
14}



15
16public partial class RecurringOrder : Order
17{






18    public TimeSpan Interval { get; }
19
20    public RecurringOrder( string customerId, TimeSpan interval )
21        : base( customerId )
22    {
23        this.Interval = interval;
24    }
25}




26
Transformed Code
1using System;
2using Metalama.Framework.RunTime;
3using Metalama.Framework.RunTime.Initialization;
4
5namespace Doc.AfterLastInstanceConstructor;
6
7[PublishWhenCreated]
8public partial class Order
9{
10    public string CustomerId { get; }
11
12    public Order(string customerId, [AspectGenerated] InitializationContext context = default)
13    {
14        this.CustomerId = customerId;
15        if (!context.IsHandled(InitializationSlot.OnConstructed))
16        {
17            this.OnConstructed(context);
18        }
19    }
20
21    protected virtual void OnConstructed(InitializationContext context = default)
22    {
23        DomainEvents.Publish(new EntityCreated("Order", this));
24    }
25}
26
27public partial class RecurringOrder : Order
28{
29    public TimeSpan Interval { get; }
30
31    public RecurringOrder(string customerId, TimeSpan interval, [AspectGenerated] InitializationContext context = default)
32        : base(customerId, context.Descend(InitializationSlot.OnConstructed))
33    {
34        this.Interval = interval;
35        if (!context.IsHandled(InitializationSlot.OnConstructed))
36        {
37            this.OnConstructed(context);
38        }
39    }
40}
41
1using System;
2
3namespace Doc.AfterLastInstanceConstructor;
4
5internal class Program
6{
7    private static void Main()
8    {
9        DomainEvents.Published +=
10            e => Console.WriteLine( $"Published: {e.TypeName}." );
11
12        _ = new Order( "alice" );
13        _ = new RecurringOrder( "bob", TimeSpan.FromDays( 30 ) );
14    }
15}
16
1using System;
2
3namespace Doc.AfterLastInstanceConstructor;
4
5public record EntityCreated( string TypeName, object Entity );
6
7public static class DomainEvents
8{
9    public static event Action<EntityCreated>? Published;
10
11    public static void Publish( EntityCreated e ) => Published?.Invoke( e );
12}
13
Published: Order.
Published: Order.

After object initialization

An object initializer is the { ... } block that follows a new expression and assigns values to accessible fields or properties, for example new Document { Id = "doc-1", Title = "Report" }. The assignments run after the constructor has returned, so any logic placed at the end of the constructor cannot see those values.

To inject logic that runs after the constructor and any object initializer or with expression has completed, use InitializerKind.AfterObjectInitializer. This is the only reliable way to validate or compute derived state after all properties and fields have been set, including those assigned via object initializers.

  1. Add a template method to your aspect class and annotate it with [Template].
  2. Call the AddInitializer method with the value InitializerKind.AfterObjectInitializer.

Metalama makes the target type implement the IInitializable interface, which defines an <xref:Metalama.Framework.RunTime.Initialization.IInitializable.Initialize> method. This method is called automatically after construction and object initialization by the framework's call-site rewriting.

For non-sealed types, the Initialize method is virtual, allowing derived types to override it and call base.Initialize(...) to chain initialization logic.

By default, the aspect's statements are appended to Initialize after the call to base.Initialize(...), in aspect-application order (i.e. CompileTime order). This can be customized through the InitializerPosition argument of AddInitializer; see Adding initializers for details.

Example: Publishing after initialization

The next aspect is a variation of the previous one: it publishes the event after the object initializer has run, so the payload can depend on properties set in the object initializer. The Id of a Document is only known once the object initializer has assigned it, so the publish cannot happen at the end of the constructor.

With AfterObjectInitializer, the aspect implements IInitializable on the target type, and the framework rewrites call sites (see the Program Code) such as new Document { Id = "..." } or the with expression to invoke Initialize after the { ... } block.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4
5namespace Doc.AfterObjectInitializer;
6
7public class PublishWhenInitializedAttribute : TypeAspect
8{
9    public override void BuildAspect( IAspectBuilder<INamedType> builder )
10    {
11        builder.AddInitializer(
12            nameof(this.OnInitialized),
13            InitializerKind.AfterObjectInitializer );
14    }
15
16    [Template]
17    private void OnInitialized()
18    {
19        DomainEvents.Publish(
20            new EntityInitialized( meta.Target.Type.Name, meta.This ) );
21    }
22}
23
Source Code
1using System;
2
3namespace Doc.AfterObjectInitializer;

4
5[PublishWhenInitialized]
6public partial record Document
7{
8    public required string Id { get; init; }
9
10    public string Title { get; init; } = "Untitled";
11}
12





13public partial record Report : Document
14{
15    public required DateOnly Date { get; init; }
16}
17
Transformed Code
1using System;
2using Metalama.Framework.RunTime.Initialization;
3
4namespace Doc.AfterObjectInitializer;
5
6[PublishWhenInitialized]
7public partial record Document : IInitializable
8{
9    public required string Id { get; init; }
10
11    public string Title { get; init; } = "Untitled";
12
13    public virtual void Initialize(InitializationContext context = default)
14    {
15        DomainEvents.Publish(new EntityInitialized("Document", this));
16    }
17}
18
19public partial record Report : Document
20{
21    public required DateOnly Date { get; init; }
22}
23
Source Code
1using System;
2
3namespace Doc.AfterObjectInitializer;
4
5internal class Program
6{
7    private static void Main()
8    {
9        DomainEvents.Published +=
10            e => Console.WriteLine( $"Published: {e.TypeName}." );
11
12        var doc = new Document { Id = "doc-1", Title = "Spec" };
13        _ = new Report { Id = "r-1", Date = new DateOnly( 2026, 4, 15 ) };
14        _ = doc with { Id = "doc-2" };
15    }
16}
17
Transformed Code
1using System;
2using Metalama.Framework.RunTime.Initialization;
3
4namespace Doc.AfterObjectInitializer;
5
6internal class Program
7{
8    private static void Main()
9    {
10        DomainEvents.Published +=
11            e => Console.WriteLine($"Published: {e.TypeName}.");
12
13        var doc = new Document { Id = "doc-1", Title = "Spec" }.WithInitialize();
14        _ = new Report { Id = "r-1", Date = new DateOnly(2026, 4, 15) }.WithInitialize();
15        _ = (doc with { Id = "doc-2" }).WithInitialize(InitializationMetadata.Modify);
16    }
17}
18
1using System;
2
3namespace Doc.AfterObjectInitializer;
4
5public record EntityInitialized( string TypeName, object Entity );
6
7public static class DomainEvents
8{
9    public static event Action<EntityInitialized>? Published;
10
11    public static void Publish( EntityInitialized e ) => Published?.Invoke( e );
12}
13
Published: Document.
Published: Document.
Published: Document.

Combining hand-written initialization logic with aspect-generated one

When the target type supplies its own OnConstructed or Initialize method, Metalama merges the aspect's statements into the user's method rather than replacing it. The user's body plays the role of the base-call anchor: BeforeBase statements are prepended to it, AfterBase statements are appended at a generated end: label, and any top-level return; in the user body is rewritten to goto end; so that appended statements still run.

Example: Tracking lifecycle with all three initializer kinds

The following TrackLifecycle aspect registers the instance's lifecycle state (BeingConstructed, Constructed, Initialized) in a static registry, using BeforeInstanceConstructor, AfterLastInstanceConstructor, and AfterObjectInitializer respectively. The Customer target supplies its own OnConstructed (which freezes a mutable tag collection) and Initialize (which performs cross-property validation).

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4
5namespace Doc.MixedInitialization;
6
7public class TrackLifecycleAttribute : TypeAspect
8{
9    public override void BuildAspect( IAspectBuilder<INamedType> builder )
10    {
11        builder.AddInitializer(
12            nameof(this.OnBeforeConstruction),
13            InitializerKind.BeforeInstanceConstructor );
14
15        builder.AddInitializer(
16            nameof(this.OnConstructed),
17            InitializerKind.AfterLastInstanceConstructor );
18
19        builder.AddInitializer(
20            nameof(this.OnInitialized),
21            InitializerKind.AfterObjectInitializer );
22    }
23
24    [Template]
25    private void OnBeforeConstruction()
26        => LifecycleRegistry.SetState( meta.This, LifecycleState.BeingConstructed );
27
28    [Template]
29    private void OnConstructed()
30        => LifecycleRegistry.SetState( meta.This, LifecycleState.Constructed );
31
32    [Template]
33    private void OnInitialized()
34        => LifecycleRegistry.SetState( meta.This, LifecycleState.FullyInitialized );
35}
36
Source Code
1using System;
2using System.Collections.Generic;
3using Metalama.Framework.RunTime.Initialization;
4
5namespace Doc.MixedInitialization;
6
7[TrackLifecycle]
8public sealed partial class Customer : IInitializable
9{
10    private List<string>? _tags = new();
11
12    public Customer( int id )
13    {
14        this.Id = id;
15    }

16

17    public int Id { get; }
18
19    public string FirstName { get; init; } = "";
20
21    public string LastName { get; init; } = "";
22
23    public string Email { get; init; } = "";
24
25    public IReadOnlyList<string> Tags { get; private set; } = null!;
26
27    public void OnConstructed( InitializationContext context = default )
28    {
29        // Once all constructors have run, the tag list is frozen.
30        this.Tags = ( this._tags ?? new List<string>() ).AsReadOnly();

31        this._tags = null;
32    }
33

34    public void Initialize( InitializationContext context = default )
35    {
36        // Cross-property validation: identity requires Email, or both names.
37        var hasEmail = !string.IsNullOrEmpty( this.Email );

38        var hasFullName = !string.IsNullOrEmpty( this.FirstName )
39                          && !string.IsNullOrEmpty( this.LastName );
40
41        if ( !hasEmail && !hasFullName )
42        {
43            throw new InvalidOperationException(
44                "A customer needs either an Email or both FirstName and LastName." );
45        }
46    }
47}


48
Transformed Code
1using System;
2using System.Collections.Generic;
3using Metalama.Framework.RunTime.Initialization;
4
5namespace Doc.MixedInitialization;
6
7[TrackLifecycle]
8public sealed partial class Customer : IInitializable
9{
10    private List<string>? _tags = new();
11
12    public Customer(int id, InitializationContext context = default)
13    {
14        LifecycleRegistry.SetState(this, LifecycleState.BeingConstructed);
15        this.Id = id;
16        this.OnConstructed(context);
17    }
18
19    public int Id { get; }
20
21    public string FirstName { get; init; } = "";
22
23    public string LastName { get; init; } = "";
24
25    public string Email { get; init; } = "";
26
27    public IReadOnlyList<string> Tags { get; private set; } = null!;
28
29    public void OnConstructed(InitializationContext context = default)
30    {
31
32        // Once all constructors have run, the tag list is frozen.
33        this.Tags = (this._tags ?? new List<string>()).AsReadOnly();
34        this._tags = null;
35        LifecycleRegistry.SetState(this, LifecycleState.Constructed);
36    }
37
38    public void Initialize(InitializationContext context = default)
39    {
40
41        // Cross-property validation: identity requires Email, or both names.
42        var hasEmail = !string.IsNullOrEmpty(this.Email);
43        var hasFullName = !string.IsNullOrEmpty(this.FirstName)
44                          && !string.IsNullOrEmpty(this.LastName);
45
46        if (!hasEmail && !hasFullName)
47        {
48            throw new InvalidOperationException(
49                "A customer needs either an Email or both FirstName and LastName.");
50        }
51
52        LifecycleRegistry.SetState(this, LifecycleState.FullyInitialized);
53    }
54}
55
Source Code
1using System;
2
3namespace Doc.MixedInitialization;
4
5internal class Program
6{
7    private static void Main()
8    {
9        var customer = new Customer( 1 ) { FirstName = "Alice", LastName = "Smith" };
10
11        Console.WriteLine( $"State: {LifecycleRegistry.GetState( customer )}" );
12    }
13}
14
Transformed Code
1using System;
2using Metalama.Framework.RunTime.Initialization;
3
4namespace Doc.MixedInitialization;
5
6internal class Program
7{
8    private static void Main()
9    {
10        var customer = new Customer(1, context: InitializationContext.WillInitialize) { FirstName = "Alice", LastName = "Smith" }.WithInitialize();
11
12        Console.WriteLine($"State: {LifecycleRegistry.GetState(customer)}");
13    }
14}
15
1using System.Collections.Generic;
2
3namespace Doc.MixedInitialization;
4
5public enum LifecycleState
6{
7    BeingConstructed,
8    Constructed,
9    FullyInitialized
10}
11
12public static class LifecycleRegistry
13{
14    private static readonly Dictionary<object, LifecycleState> _states = new();
15
16    public static void SetState( object instance, LifecycleState state )
17    {
18        lock ( _states )
19        {
20            _states[instance] = state;
21        }
22    }
23
24    public static LifecycleState? GetState( object instance )
25    {
26        lock ( _states )
27        {
28            return _states.TryGetValue( instance, out var state ) ? state : null;
29        }
30    }
31}
32
State: FullyInitialized

Ordering of initializers

When several aspects add initializers to the same type, Metalama lays out their statements according to the matryoshka rule: statements placed before the base call run in reverse aspect-application order, and statements placed after the base call run in direct aspect-application order. This mirrors the ordering of a method-override chain, where outer (more-derived) logic wraps inner (base) logic: the outer layer's pre-base code runs first on the way in, and the inner layer's post-base code runs first on the way out.

Given [assembly: AspectOrder(AspectOrderDirection.RunTime, typeof(FirstAspect), typeof(SecondAspect))] (i.e. FirstAspect is the outer layer and runs first at run time), a before-base statement from FirstAspect runs before one from SecondAspect, and an after-base statement from SecondAspect runs before one from FirstAspect.

How this applies to each <xref:Metalama.Framework.Aspects.InitializerKind>:

  • <xref:Metalama.Framework.Aspects.InitializerKind.BeforeTypeConstructor>: direct aspect-application order. No base call is involved.
  • <xref:Metalama.Framework.Aspects.InitializerKind.BeforeInstanceConstructor>: direct aspect-application order. The advice sits after the constructor's :base(...) call, so it falls in the after-base bucket.
  • <xref:Metalama.Framework.Aspects.InitializerKind.AfterLastInstanceConstructor> and <xref:Metalama.Framework.Aspects.InitializerKind.AfterObjectInitializer>: governed by the InitializerPosition argument. AfterBase (the default) runs in direct aspect-application order after base.OnConstructed(...) / base.Initialize(...); BeforeBase runs in reverse aspect-application order before that call. In a sealed class the base call does not exist, so BeforeBase simply means "reverse order across aspect instances" and AfterBase means "direct order across aspect instances".

Within a single aspect instance, multiple calls to AddInitializer preserve their programmatic add-order inside each bucket.

Example: Two aspects and two levels of inheritance

The following example exercises the matryoshka rule end-to-end. AspectA and AspectB are both [Inheritable] and each adds a BeforeBase and an AfterBase initializer for AfterLastInstanceConstructor. The assembly-level AspectOrder declares AspectA as the outer layer (run-time-first). BaseClass carries both attributes and DerivedClass inherits them. Running new DerivedClass() produces the expected order: DerivedClass's OnConstructed runs its pre-base statements (outer-first: A then B), calls base.OnConstructed(), which runs its own pre-base statements, then unwinds its post-base statements (inner-first: B then A), and finally DerivedClass's post-base statements run in the same inner-first order.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5
6[assembly: AspectOrder(
7    AspectOrderDirection.RunTime,
8    typeof(Doc.InitializerOrdering.AspectAAttribute),
9    typeof(Doc.InitializerOrdering.AspectBAttribute) )]
10
11namespace Doc.InitializerOrdering;
12
13[Inheritable]
14public class AspectAAttribute : TypeAspect
15{
16    public override void BuildAspect( IAspectBuilder<INamedType> builder )
17    {
18        builder.AddInitializer(
19            nameof(this.Before),
20            InitializerKind.AfterLastInstanceConstructor,
21            InitializerPosition.BeforeBase );
22
23        builder.AddInitializer(
24            nameof(this.After),
25            InitializerKind.AfterLastInstanceConstructor,
26            InitializerPosition.AfterBase );
27    }
28
29    [Template]
30    private void Before()
31        => Console.WriteLine( $"AspectA before in {meta.Target.Type.Name}" );
32
33    [Template]
34    private void After()
35        => Console.WriteLine( $"AspectA after in {meta.Target.Type.Name}" );
36}
37
38[Inheritable]
39public class AspectBAttribute : TypeAspect
40{
41    public override void BuildAspect( IAspectBuilder<INamedType> builder )
42    {
43        builder.AddInitializer(
44            nameof(this.Before),
45            InitializerKind.AfterLastInstanceConstructor,
46            InitializerPosition.BeforeBase );
47
48        builder.AddInitializer(
49            nameof(this.After),
50            InitializerKind.AfterLastInstanceConstructor,
51            InitializerPosition.AfterBase );
52    }
53
54    [Template]
55    private void Before()
56        => Console.WriteLine( $"AspectB before in {meta.Target.Type.Name}" );
57
58    [Template]
59    private void After()
60        => Console.WriteLine( $"AspectB after in {meta.Target.Type.Name}" );
61}
62
Source Code
1using System;
2
3namespace Doc.InitializerOrdering;


4
5[AspectA]
6[AspectB]
7public partial class BaseClass
8{
9    public BaseClass()
10    {


11        Console.WriteLine( "BaseClass constructor" );
12    }

13}
14
15public partial class DerivedClass : BaseClass
16{









17    public DerivedClass()
18    {
19        Console.WriteLine( "DerivedClass constructor" );
20    }









21}




22
Transformed Code
1using System;
2using Metalama.Framework.RunTime;
3using Metalama.Framework.RunTime.Initialization;
4
5namespace Doc.InitializerOrdering;
6
7[AspectA]
8[AspectB]
9public partial class BaseClass
10{
11    public BaseClass([AspectGenerated] InitializationContext context = default)
12    {
13        Console.WriteLine("BaseClass constructor");
14        if (!context.IsHandled(InitializationSlot.OnConstructed))
15        {
16            this.OnConstructed(context);
17        }
18    }
19
20    protected virtual void OnConstructed(InitializationContext context = default)
21    {
22        Console.WriteLine("AspectA before in BaseClass");
23        Console.WriteLine("AspectB before in BaseClass");
24        Console.WriteLine("AspectB after in BaseClass");
25        Console.WriteLine("AspectA after in BaseClass");
26    }
27}
28
29public partial class DerivedClass : BaseClass
30{
31    public DerivedClass([AspectGenerated] InitializationContext context = default) : base(context.Descend(InitializationSlot.OnConstructed))
32    {
33        Console.WriteLine("DerivedClass constructor");
34        if (!context.IsHandled(InitializationSlot.OnConstructed))
35        {
36            this.OnConstructed(context);
37        }
38    }
39
40    protected override void OnConstructed(InitializationContext context = default)
41    {
42        Console.WriteLine("AspectA before in DerivedClass");
43        Console.WriteLine("AspectB before in DerivedClass");
44        base.OnConstructed(context);
45        Console.WriteLine("AspectB after in DerivedClass");
46        Console.WriteLine("AspectA after in DerivedClass");
47    }
48}
49
1namespace Doc.InitializerOrdering;
2
3internal class Program
4{
5    private static void Main()
6    {
7        _ = new DerivedClass();
8    }
9}
10
BaseClass constructor
DerivedClass constructor
AspectA before in DerivedClass
AspectB before in DerivedClass
AspectA before in BaseClass
AspectB before in BaseClass
AspectB after in BaseClass
AspectA after in BaseClass
AspectB after in DerivedClass
AspectA after in DerivedClass

Running an initializer only at the most-derived layer

The generated Initialize and OnConstructed methods are declared virtual, so a derived class invokes base.Initialize(...) or base.OnConstructed(...) from its own override. When an [Inheritable] aspect is applied to a base class, the derived class inherits the aspect's template body at every level of the hierarchy, and constructing a derived instance executes that body once per level. For logic that depends on the object being fully initialized (for example, external validation of the completed aggregate, publication of a single "created" domain event, or freezing), execution at every inheritance level is incorrect: the base levels observe only a partial view of the object.

Metalama resolves this through initialization slots. An initialization slot is a marker indicating that a given concern is handled by the derived method and must therefore be skipped by the base method. Each concern (typically one per aspect) is assigned its own InitializationSlot, and the framework propagates it to base levels through an InitializationContext parameter added to every generated Initialize and OnConstructed method.

Orchestrating initialization with initialization slots

Three steps are required:

  1. Define a public static field of type InitializationSlot and initialize it by calling Allocate.
  2. Pass the corresponding IField to the AddInitializer method via the slotFields: parameter (one aspect may pass several slots).
  3. In the template, accept an InitializationContext parameter and guard the body with if (!context.IsHandled(slot)) { ... }.

Generated code

The slotFields: parameter on AddInitializer determines the code that Metalama emits on derived types. Without slot fields, a derived Initialize forwards the incoming context unchanged: base.Initialize(context). With slot fields, Metalama rewrites the call to invoke Descend, combining all slots declared by slot-using aspects on the type with the | operator. For the two aspects in the example below, SubscriptionOrder.Initialize emits base.Initialize(context.Descend(InitializerSlots.Validate | InitializerSlots.Publish)). Descend returns a copy of the context with the specified slots added to its handled set. When the base-level template evaluates context.IsHandled(slot), the guard returns true and the body is skipped. The derived-level template receives the original (unmodified) context, so its body executes. Descend is invoked exclusively by framework-generated code; aspect authors do not call it directly.

Additional notes

  • Up to 32 slots can be allocated per application domain. This limit is sufficient for typical use: most applications allocate only a small number of slots.
  • The framework reserves one slot, InitializationSlot.OnConstructed, to prevent duplicate OnConstructed calls across this(...) constructor chains. This slot is reserved for internal framework use and is not intended for aspect code.
  • The mechanism applies to both InitializerKind.AfterObjectInitializer (through IInitializable.Initialize) and InitializerKind.AfterLastInstanceConstructor (through the generated virtual OnConstructed).

Example: validating the finished object before publishing

The following example applies two [Inheritable] aspects to an Order hierarchy. [Validate] invokes an external ValidationService on the fully-initialized object, and [Publish] raises a "created" event through an external PublishService. Each aspect declares its own slot, so each executes exactly once per constructed object, at the most-derived level, once every init property has been assigned.

The declaration [assembly: AspectOrder(AspectOrderDirection.RunTime, typeof(PublishAttribute), typeof(ValidateAttribute))] designates Publish as the outer aspect at run time, which causes Validate to execute before Publish at the most-derived level. The event is therefore raised only for an object that has already been validated.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.RunTime.Initialization;
5using System.Linq;
6
7[assembly: AspectOrder(
8    AspectOrderDirection.RunTime,
9    typeof(Doc.InitializerSlot.PublishAttribute),
10    typeof(Doc.InitializerSlot.ValidateAttribute) )]
11
12namespace Doc.InitializerSlot;
13
14[Inheritable]
15public class ValidateAttribute : TypeAspect
16{
17    public override void BuildAspect( IAspectBuilder<INamedType> builder )
18    {
19        var slotField = TypeFactory.GetNamedType( typeof(InitializerSlots) )
20            .Fields.OfName( nameof(InitializerSlots.Validate) ).Single();
21
22        builder.AddInitializer(
23            nameof(this.Template),
24            InitializerKind.AfterObjectInitializer,
25            slotFields: new[] { slotField } );
26    }
27
28    [Template]
29    private void Template( InitializationContext context )
30    {
31        if ( !context.IsHandled( InitializerSlots.Validate ) )
32        {
33            ValidationService.Validate( meta.This );
34        }
35    }
36}
37
38[Inheritable]
39public class PublishAttribute : TypeAspect
40{
41    public override void BuildAspect( IAspectBuilder<INamedType> builder )
42    {
43        var slotField = TypeFactory.GetNamedType( typeof(InitializerSlots) )
44            .Fields.OfName( nameof(InitializerSlots.Publish) ).Single();
45
46        builder.AddInitializer(
47            nameof(this.Template),
48            InitializerKind.AfterObjectInitializer,
49            slotFields: new[] { slotField } );
50    }
51
52    [Template]
53    private void Template( InitializationContext context )
54    {
55        if ( !context.IsHandled( InitializerSlots.Publish ) )
56        {
57            PublishService.Publish( meta.This );
58        }
59    }
60}
61
Source Code
1namespace Doc.InitializerSlot;
2


3[Validate]
4[Publish]
5public partial class Order
6{
7    public string OrderId { get; init; } = "";
8
9    public string CustomerId { get; init; } = "";
10}
11






12public partial class SubscriptionOrder : Order
13{







14    public int RenewalIntervalDays { get; init; }
15}
16
Transformed Code
1using Metalama.Framework.RunTime.Initialization;
2
3namespace Doc.InitializerSlot;
4
5[Validate]
6[Publish]
7public partial class Order : IInitializable
8{
9    public string OrderId { get; init; } = "";
10
11    public string CustomerId { get; init; } = "";
12
13    public virtual void Initialize(InitializationContext context = default)
14    {
15        if (!context.IsHandled(InitializerSlots.Validate))
16        {
17            ValidationService.Validate(this);
18        }
19
20        if (!context.IsHandled(InitializerSlots.Publish))
21        {
22            PublishService.Publish(this);
23        }
24    }
25}
26
27public partial class SubscriptionOrder : Order
28{
29    public int RenewalIntervalDays { get; init; }
30
31    public override void Initialize(InitializationContext context = default)
32    {
33        base.Initialize(context.Descend(InitializerSlots.Validate | InitializerSlots.Publish));
34        if (!context.IsHandled(InitializerSlots.Validate))
35        {
36            ValidationService.Validate(this);
37        }
38
39        if (!context.IsHandled(InitializerSlots.Publish))
40        {
41            PublishService.Publish(this);
42        }
43    }
44}
45
Source Code
1namespace Doc.InitializerSlot;
2
3internal class Program
4{
5    private static void Main()
6    {
7        _ = new Order { OrderId = "o-1", CustomerId = "alice" };
8
9        _ = new SubscriptionOrder
10        {
11            OrderId = "o-2",
12            CustomerId = "bob",
13            RenewalIntervalDays = 30
14        };
15    }
16}
17
Transformed Code
1using Metalama.Framework.RunTime.Initialization;
2
3namespace Doc.InitializerSlot;
4
5internal class Program
6{
7    private static void Main()
8    {
9        _ = new Order { OrderId = "o-1", CustomerId = "alice" }.WithInitialize();
10
11        _ = new SubscriptionOrder
12        {
13            OrderId = "o-2",
14            CustomerId = "bob",
15            RenewalIntervalDays = 30
16        }.WithInitialize();
17    }
18}
19
1using Metalama.Framework.RunTime.Initialization;
2using System;
3
4namespace Doc.InitializerSlot;
5
6// Slots live on a plain (non-[CompileTime]) static holder because aspect types are
7// [CompileTime], so their static fields cannot flow into run-time template code.
8public static class InitializerSlots
9{
10    public static readonly InitializationSlot Validate = InitializationSlot.Allocate();
11
12    public static readonly InitializationSlot Publish = InitializationSlot.Allocate();
13}
14
15public static class ValidationService
16{
17    public static void Validate( object entity )
18        => Console.WriteLine( $"Validated {entity.GetType().Name}" );
19}
20
21public static class PublishService
22{
23    public static void Publish( object entity )
24        => Console.WriteLine( $"Published {entity.GetType().Name}" );
25}
26
Validated Order
Published Order
Validated SubscriptionOrder
Published SubscriptionOrder