Open sandboxFocusImprove this doc

Introducing members

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}
Source Code
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}
Transformed Code
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:

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}
Source Code
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}
Transformed Code
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 IsPartial or IsAbstract property of the [Template] attribute.
  • Set the IsPartial or IsAbstract property 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.

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}
Source Code
1namespace Doc.IntroducePropertyChanged1;
2


3[IntroducePropertyChangedAspect]
4internal class Foo { }
Transformed Code
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}
Source Code
1namespace Doc.IntroducePropertyChanged3;
2


3[IntroducePropertyChangedAspect]
4internal class Foo { }
Transformed Code
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}
Source Code
1namespace Doc.IntroducePropertyChanged2;
2


3[IntroducePropertyChangedAspect]
4internal class Foo { }
Transformed Code
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.

  • Entity is a base class without the aspect but with its own OnPropertyChanged implementation.
  • Customer derives from Entity and has the aspect. The aspect overrides OnPropertyChanged and calls base.OnPropertyChanged.
  • StandaloneOrder has the aspect but no base class. The aspect introduces OnPropertyChanged as 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
Source Code
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
Transformed Code
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.