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
, 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.
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}
1namespace Doc.IntroduceId
2{
3 [IntroduceId]
4 internal class MyClass { }
5}
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
You can also use the T# template language 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}
1namespace Doc.BuildInfo
2{
3 internal partial class BuildInfo { }
4}
1namespace Doc.BuildInfo
2{
3
4#pragma warning disable CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
5 internal partial class BuildInfo
6 {
7 public string? Configuration { get; } = "Debug";
8
9
10 public string? TargetFramework { get; } = "net6.0";
11 }
12
13#pragma warning restore CS0067, CS8618, CS0162, CS0169, CS0414, CA1822, CA1823, IDE0051, IDE0052
14
15}
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
The aspect in the following example 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}
1namespace Doc.ProgrammaticInitializer
2{
3 [AddMethodNamesAspect]
4 internal class Foo
5 {
6 private void M1() { }
7
8 private void M2() { }
9 }
10}
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 {
12 "M1",
13 "M2"
14 };
15 }
16}
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 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 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{
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}
1namespace Doc.RegisterInstance
2{
3 [RegisterInstance]
4 internal class DemoClass
5 {
6 public DemoClass() : base() {}
7 public DemoClass(int i) : this() {}
8 public DemoClass(string s) {}
9 }
10}
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 public DemoClass(string s)
15 {
16 this._instanceRegistryHandle = InstanceRegistry.Register(this);
17 }
18
19 private IDisposable _instanceRegistryHandle;
20 }
21}
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}
Allocate object. Doc.RegisterInstance.DemoClass GC.Collect()
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.
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. The InitializerType.BeforeTypeConstructor
value needs to be used instead.