In previous articles, you learned how to override existing type members. This article explains how to add new members to a type.
You can add the following types of members:
- Methods
- Constructors
- Fields
- Properties
- Events
- Operators
- Conversions
Introducing members declaratively
The simplest way to introduce a member from an aspect is to implement it in the aspect and annotate it with the [Introduce] custom attribute, which has these notable properties:
| Property | Description |
|---|---|
| Name | Sets the name of the introduced member. If not specified, the name of the introduced member is the name of the template itself. |
| Scope | Determines whether the introduced member will be static. See IntroductionScope for possible strategies. By default, copies from the template, except when you apply the aspect to a static member, which always makes the introduced member static. |
| Accessibility | Determines the member's accessibility (private, protected, public, etc.). By default, copies the template's accessibility. |
| IsVirtual | Determines whether the member will be virtual. By default, copies the template's characteristic. |
| IsSealed | Determines whether the member will be sealed. By default, copies the template's characteristic. |
Note
Constructors can't be introduced declaratively.
Example: ToString
This example shows an aspect that implements the ToString method, returning a string with the object type and a unique identifier.
This aspect replaces any hand-written implementation of ToString, which isn't desirable. To avoid this, introduce the method programmatically and conditionally.
1using Metalama.Framework.Aspects;
2
3namespace Doc.IntroduceMethod;
4
5internal class ToStringAttribute : TypeAspect
6{
7 [Introduce]
8 private readonly int _id = IdGenerator.GetId();
9
10 [Introduce( WhenExists = OverrideStrategy.Override )]
11 public override string ToString() => $"{this.GetType().Name} Id={this._id}";
12}
1using System;
2using System.Threading;
3
4namespace Doc.IntroduceMethod;
5
6[ToString]
7internal class MyClass { }
8
9internal static class IdGenerator
10{
11 private static int _nextId;
12
13 public static int GetId() => Interlocked.Increment( ref _nextId );
14}
15
16internal class Program
17{
18 private static void Main()
19 {
20 Console.WriteLine( new MyClass().ToString() );
21 Console.WriteLine( new MyClass().ToString() );
22 Console.WriteLine( new MyClass().ToString() );
23 }
24}
1using System;
2using System.Threading;
3
4namespace Doc.IntroduceMethod;
5
6[ToString]
7internal class MyClass
8{
9 private readonly int _id = IdGenerator.GetId();
10
11 public override string ToString()
12 {
13 return $"{GetType().Name} Id={_id}";
14 }
15}
16
17internal static class IdGenerator
18{
19 private static int _nextId;
20
21 public static int GetId() => Interlocked.Increment(ref _nextId);
22}
23
24internal class Program
25{
26 private static void Main()
27 {
28 Console.WriteLine(new MyClass().ToString());
29 Console.WriteLine(new MyClass().ToString());
30 Console.WriteLine(new MyClass().ToString());
31 }
32}
MyClass Id=1 MyClass Id=2 MyClass Id=3
Introducing members programmatically
The main limitation of declarative introductions is that you must know the name, type, and signature of the introduced member upfront—they can't depend on the aspect target. The programmatic approach lets your aspect fully customize the declaration based on the target code.
There are two steps to introduce a member programmatically:
Step 1. Implement the template
Implement the template in your aspect class and annotate it with the [Template] custom attribute. The template doesn't need the final signature.
Step 2. Invoke AdviserExtensions.Introduce*
In your implementation of the BuildAspect method, call one of the following methods and store the return value in a variable:
- IntroduceMethod returning an IMethodBuilder
- IntroduceProperty returning an IPropertyBuilder
- IntroduceEvent returning an IEventBuilder
- IntroduceField returning an IFieldBuilder
- IntroduceConstructor returning an IConstructorBuilder
These methods create a member with the same characteristics as the template (name, signature, etc.), accounting for the [Template] custom attribute properties.
To modify the name and signature of the introduced declaration, use the buildMethod, buildProperty, buildEvent, buildField, or buildConstructor parameter of the Introduce* method.
Example: Update method
The following aspect introduces an Update method that assigns all writable fields in the target type. The method signature is dynamic: there is one parameter per writable field or property.
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System.Linq;
5
6namespace Doc.UpdateMethod;
7
8internal class UpdateMethodAttribute : TypeAspect
9{
10 public override void BuildAspect( IAspectBuilder<INamedType> builder )
11 {
12 builder.IntroduceMethod(
13 nameof(this.Update),
14 buildMethod:
15 m =>
16 {
17 var fieldsAndProperties =
18 builder.Target.FieldsAndProperties
19 .Where( f => f is
20 {
21 IsImplicitlyDeclared: false, Writeability: Writeability.All
22 } );
23
24 foreach ( var field in fieldsAndProperties )
25 {
26 m.AddParameter( field.Name, field.Type );
27 }
28 } );
29 }
30
31 [Template]
32 public void Update()
33 {
34 var index = meta.CompileTime( 0 );
35
36 foreach ( var parameter in meta.Target.Parameters )
37 {
38 var field = meta.Target.Type.FieldsAndProperties.OfName( parameter.Name ).Single();
39
40 field.Value = meta.Target.Parameters[index].Value;
41 index++;
42 }
43 }
44}
1using System;
2
3namespace Doc.UpdateMethod;
4
5[UpdateMethod]
6internal partial class CityHunter
7{
8 private int _x;
9
10 public string? Y { get; private set; }
11
12 public DateTime Z { get; }
13}
14
15internal class Program
16{
17 private static void Main()
18 {
19 CityHunter ch = new();
20 ch.Update(0, "1");
21 }
22}
1using System;
2
3namespace Doc.UpdateMethod;
4
5[UpdateMethod]
6internal partial class CityHunter
7{
8 private int _x;
9
10 public string? Y { get; private set; }
11
12 public DateTime Z { get; }
13
14 public void Update(int _x, string? Y)
15 {
16 this._x = _x;
17 this.Y = Y;
18 }
19}
20
21internal class Program
22{
23 private static void Main()
24 {
25 CityHunter ch = new();
26 ch.Update(0, "1");
27 }
28}
Introducing a partial or abstract member
You can use any Introduce* method to add a partial or abstract member. However, the template itself can't be partial or extern because that wouldn't be valid C#.
There are two ways to make a member partial or abstract:
- Set the
IsPartialorIsAbstractproperty of the[Template]attribute. - Set the
IsPartialorIsAbstractproperty of the IMemberBuilder object.
The implementation body of the template will be ignored if you set the IsAbstract or IsPartial property, so any implementation will do. However, if you don't want to have any body, you can use the extern keyword on the template member. This keyword will be removed during compilation, and dummy implementations will be provided.
Overriding existing implementations
Specifying the override strategy
When you introduce a member to a type, the same member might already exist in that type or a parent type. The default strategy reports an error and fails the build. Change this behavior by setting the OverrideStrategy for the advice:
- For declarative advice, set the WhenExists property of the custom attribute.
- For programmatic advice, set the whenExists optional parameter of the advice factory method.
Accessing the overridden declaration
When you override a method, you'll usually want to invoke the base implementation. The same applies to properties and events. In plain C#, you use the base prefix. Metalama uses a similar approach.
- To invoke the base method or accessor with exactly the same arguments, call meta.Proceed.
- To invoke the base method with different arguments, use meta.Target.Method.Invoke.
- To call the base property getter or setter, use meta.Property.Value.
- To access the base event, use meta.Event.Add, meta.Event.Remove or meta.Event.Raise.
For details, see Generating code based on the code model.
Note
Inside an override template, invokers resolve to the previous implementation layer by default—that is, the implementation before the current aspect, or the base implementation if you're the first aspect in the chain. In other contexts, invokers resolve to the final layer, which includes all aspect transformations and uses virtual dispatch when applicable. To specify a different layer, use the WithOptions method with the appropriate InvokerOptions value.
Referencing introduced members in a template
When you introduce a member to a type, you'll often want to access it from templates. You can do this three ways:
Option 1. Access the aspect template member
1using Metalama.Framework.Aspects;
2using System.ComponentModel;
3
4namespace Doc.IntroducePropertyChanged1;
5
6internal class IntroducePropertyChangedAspect : TypeAspect
7{
8 [Introduce]
9 public event PropertyChangedEventHandler? PropertyChanged;
10
11 [Introduce]
12 protected virtual void OnPropertyChanged( string propertyName )
13 {
14 this.PropertyChanged?.Invoke( meta.This, new PropertyChangedEventArgs( propertyName ) );
15 }
16}
1namespace Doc.IntroducePropertyChanged1;
2
3[IntroducePropertyChangedAspect]
4internal class Foo { }
1using System.ComponentModel;
2
3namespace Doc.IntroducePropertyChanged1;
4
5[IntroducePropertyChangedAspect]
6internal class Foo
7{
8 protected virtual void OnPropertyChanged(string propertyName)
9 {
10 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
11 }
12
13 public event PropertyChangedEventHandler? PropertyChanged;
14}
Option 2. Use meta.This and write dynamic code
1using Metalama.Framework.Aspects;
2using System.ComponentModel;
3
4namespace Doc.IntroducePropertyChanged3;
5
6internal class IntroducePropertyChangedAspect : TypeAspect
7{
8 [Introduce]
9 public event PropertyChangedEventHandler? PropertyChanged;
10
11 [Introduce]
12 protected virtual void OnPropertyChanged( string propertyName )
13 {
14 meta.This.PropertyChanged?.Invoke(
15 meta.This,
16 new PropertyChangedEventArgs( propertyName ) );
17 }
18}
1namespace Doc.IntroducePropertyChanged3;
2
3[IntroducePropertyChangedAspect]
4internal class Foo { }
1using System.ComponentModel;
2
3namespace Doc.IntroducePropertyChanged3;
4
5[IntroducePropertyChangedAspect]
6internal class Foo
7{
8 protected virtual void OnPropertyChanged(string propertyName)
9 {
10 this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
11 }
12
13 public event PropertyChangedEventHandler? PropertyChanged;
14}
Option 3. Use the invoker of the builder object
If neither approach above offers the required flexibility (typically because the name of the introduced member is dynamic), use the invokers exposed on the builder object returned from the advice factory method.
Note
Declarations introduced by an aspect or aspect layer aren't visible in the meta code model exposed to the same aspect or aspect layer. You must reference them differently. For details, see Sharing state with advice.
For more details, see Metalama.Framework.Code.Invokers.
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System.ComponentModel;
5
6namespace Doc.IntroducePropertyChanged2;
7
8internal class IntroducePropertyChangedAspect : TypeAspect
9{
10 public override void BuildAspect( IAspectBuilder<INamedType> builder )
11 {
12 var propertyChangedEvent = builder.IntroduceEvent( nameof(this.PropertyChanged) )
13 .Declaration;
14
15 builder.IntroduceMethod(
16 nameof(this.OnPropertyChanged),
17 args: new { theEvent = propertyChangedEvent } );
18 }
19
20 [Template]
21 public event PropertyChangedEventHandler? PropertyChanged;
22
23 [Template]
24 protected virtual void OnPropertyChanged( string propertyName, IEvent theEvent )
25 {
26 theEvent.Raise( meta.This, new PropertyChangedEventArgs( propertyName ) );
27 }
28}
1namespace Doc.IntroducePropertyChanged2;
2
3[IntroducePropertyChangedAspect]
4internal class Foo { }
1using System.ComponentModel;
2
3namespace Doc.IntroducePropertyChanged2;
4
5[IntroducePropertyChangedAspect]
6internal class Foo
7{
8 protected virtual void OnPropertyChanged(string propertyName)
9 {
10 PropertyChanged?.Invoke((object?)this, new PropertyChangedEventArgs(propertyName));
11 }
12
13 public event PropertyChangedEventHandler? PropertyChanged;
14}
Example: Dirty tracking
The following example shows a DirtyTracking aspect that introduces an IsDirty property and a virtual OnPropertyChanged method. The aspect uses WhenExists = OverrideStrategy.Override so it can override an existing OnPropertyChanged in a base class while also introducing the method when no base exists.
Note
An optimal dirty-tracking implementation would automatically instrument property setters to call OnPropertyChanged. This example assumes you don't own the properties—for instance, they might be in a base class you can't modify—so the aspect only introduces the OnPropertyChanged hook and relies on existing code to call it. For a complete change-tracking implementation that instruments properties automatically, see Change Tracking.
Entityis a base class without the aspect but with its ownOnPropertyChangedimplementation.Customerderives fromEntityand has the aspect. The aspect overridesOnPropertyChangedand callsbase.OnPropertyChanged.StandaloneOrderhas the aspect but no base class. The aspect introducesOnPropertyChangedas a new virtual method.
1using Metalama.Framework.Aspects;
2
3namespace Doc.DirtyTracking;
4
5public class DirtyTrackingAttribute : TypeAspect
6{
7 // Introduces a property to track whether the object has been modified.
8 [Introduce]
9 public bool IsDirty { get; private set; }
10
11 // Introduces OnPropertyChanged if not present in base class, or overrides it if present.
12 // When overriding, meta.Proceed() calls the base implementation.
13 [Introduce( WhenExists = OverrideStrategy.Override )]
14 protected virtual void OnPropertyChanged( string propertyName )
15 {
16 this.IsDirty = true;
17 meta.Proceed();
18 }
19}
20
1using System;
2
3namespace Doc.DirtyTracking;
4
5// Base class without the aspect, but with OnPropertyChanged.
6internal class Entity
7{
8 protected virtual void OnPropertyChanged( string propertyName )
9 {
10 Console.WriteLine( $"Entity.OnPropertyChanged({propertyName})" );
11 }
12}
13
14// Derived class with the aspect. The aspect overrides OnPropertyChanged and calls base.
15[DirtyTracking]
16internal partial class Customer : Entity
17{
18 private string? _name;
19
20 public string? Name
21 {
22 get => _name;
23 set
24 {
25 _name = value;
26 OnPropertyChanged( nameof(Name) );
27 }
28 }
29}
30
31// Standalone class with the aspect. The aspect introduces OnPropertyChanged.
32[DirtyTracking]
33internal partial class StandaloneOrder
34{
35 private int _quantity;
36
37 public int Quantity
38 {
39 get => _quantity;
40 set
41 {
42 _quantity = value;
43 OnPropertyChanged( nameof(Quantity) );
44 }
45 }
46}
47
1using System;
2
3namespace Doc.DirtyTracking;
4
5// Base class without the aspect, but with OnPropertyChanged.
6internal class Entity
7{
8 protected virtual void OnPropertyChanged(string propertyName)
9 {
10 Console.WriteLine($"Entity.OnPropertyChanged({propertyName})");
11 }
12}
13
14// Derived class with the aspect. The aspect overrides OnPropertyChanged and calls base.
15[DirtyTracking]
16internal partial class Customer : Entity
17{
18 private string? _name;
19
20 public string? Name
21 {
22 get => _name;
23 set
24 {
25 _name = value;
26 OnPropertyChanged(nameof(Name));
27 }
28 }
29
30 public bool IsDirty { get; private set; }
31
32 protected override void OnPropertyChanged(string propertyName)
33 {
34 IsDirty = true;
35 base.OnPropertyChanged(propertyName);
36 }
37}
38
39// Standalone class with the aspect. The aspect introduces OnPropertyChanged.
40[DirtyTracking]
41internal partial class StandaloneOrder
42{
43 private int _quantity;
44
45 public int Quantity
46 {
47 get => _quantity;
48 set
49 {
50 _quantity = value;
51 OnPropertyChanged(nameof(Quantity));
52 }
53 }
54
55 public bool IsDirty { get; private set; }
56
57 protected virtual void OnPropertyChanged(string propertyName)
58 {
59 IsDirty = true;
60 }
61}
62
Referencing introduced members from source code
If you want source code (not aspect code) to reference declarations introduced by your aspect, users must make the target types partial. Without this keyword, introduced declarations won't be visible at design time in syntax completion, and the IDE will report errors.
The compiler won't complain because Metalama handles it, but the IDE will because it doesn't know about Metalama. Your aspect must follow standard C# compiler rules. Neither aspect authors nor Metalama can work around this limitation.
If the user doesn't add the partial keyword, Metalama will report a warning and offer a code fix.