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 aspect introduces 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
6internal class IntroduceIdAttribute : TypeAspect
7{
8 [Introduce]
9 public Guid Id { get; } = Guid.NewGuid();
10}
1namespace Doc.IntroduceId;
2
3[IntroduceId]
4internal class MyClass { }
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 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
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}
1namespace Doc.BuildInfo;
2
3internal partial class BuildInfo { }
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; } = "net6.0";
12}
13
14#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
15
16
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 names of all methods in the target type.
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Code.SyntaxBuilders;
5using System.Linq;
6
7namespace Doc.ProgrammaticInitializer;
8
9internal class AddMethodNamesAspect : TypeAspect
10{
11 public override void BuildAspect( IAspectBuilder<INamedType> builder )
12 {
13 // Create an expression that contains the array with all method names.
14 var arrayBuilder = new ArrayBuilder( typeof(string) );
15
16 foreach ( var methodName in builder.Target.Methods.Select( m => m.Name ).Distinct() )
17 {
18 arrayBuilder.Add( ExpressionFactory.Literal( methodName ) );
19 }
20
21 // Introduce a field and initialize it to that array.
22 builder.IntroduceField(
23 "_methodNames",
24 typeof(string[]),
25 buildField: f => f.InitializerExpression = arrayBuilder.ToExpression() );
26 }
27}
1namespace Doc.ProgrammaticInitializer;
2
3[AddMethodNamesAspect]
4internal class Foo
5{
6 private void M1() { }
7
8 private void M2() { }
9}
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 any object constructor
To inject some initialization before any user code of the instance constructor is called:
- 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. - 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 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}
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}
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()
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.