Open sandboxFocusImprove this doc

Introducing members

In previous articles, you learned how to override the implementation of existing type members. This article will teach you how to add new members to an existing type.

Currently, 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 this member in the aspect and annotate it with the [Introduce] custom attribute. This custom attribute has the following 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 or not. See IntroductionScope for possible strategies. By default, it is copied from the template, except when the aspect is applied to a static member, in which case the introduced member is always static.
Accessibility Determines if the member will be private, protected, public, etc. By default, the accessibility of the template is copied.
IsVirtual Determines if the member will be virtual. By default, the characteristic of the template is copied.
IsSealed Determines if the member will be sealed. By default, the characteristic of the template is copied.
Note

Constructors cannot be introduced declaratively.

Example: ToString

The following example demonstrates an aspect that implements the ToString method. It will return a string that includes the object type and a reasonably unique identifier for that object.

Please note that this aspect will replace any hand-written implementation of ToString, which is not desirable. Currently, this can only be avoided by introducing 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 the name, type, and signature of the introduced member must be known upfront. They cannot depend on the aspect target. The programmatic approach allows your aspect to 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 does not need to have 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:

A call to one of these methods creates a member by default that has the same characteristics as the template (name, signature, etc.), taking into account the properties of the [Template] custom attribute.

To modify the name and signature of the introduced declaration, use the buildMethod, buildProperty, buildEvent, or buildField 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(
20                            f => f is
21                            {
22                                IsImplicitlyDeclared: false, Writeability: Writeability.All
23                            } );
24
25                foreach ( var field in fieldsAndProperties )
26                {
27                    m.AddParameter( field.Name, field.Type );
28                }
29            } );
30    }
31
32    [Template]
33    public void Update()
34    {
35        var index = meta.CompileTime( 0 );
36
37        foreach ( var parameter in meta.Target.Parameters )
38        {
39            var field = meta.Target.Type.FieldsAndProperties.OfName( parameter.Name ).Single();
40
41            field.Value = meta.Target.Parameters[index].Value;
42            index++;
43        }
44    }
45}
Source Code
1using System;
2
3namespace Doc.UpdateMethod;
4
5[UpdateMethod]
6internal 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();
            Error CS1061: 'CityHunter' does not contain a definition for 'Update' and no accessible extension method 'Update' accepting a first argument of type 'CityHunter' could be found (are you missing a using directive or an assembly reference?)

20            ch.Update(0, "1");
21    }
22}
Transformed Code
1using System;
2
3namespace Doc.UpdateMethod;
4
5[UpdateMethod]
6internal 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 of the Introduce* methods to add a partial or abstract member. However, the template itself can be neither partial nor extern because that would not 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 do not 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 want to introduce a member to a type, it may happen that the same member is already defined in this type or in a parent type. The default strategy of the aspect framework in this case is simply to report an error and fail the build. You can change this behavior by setting the OverrideStrategy for this 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

Most of the time, when you override a method, you will want to invoke the base implementation. The same applies to properties and events. In plain C#, when you override a base-class member in a derived class, you call the member with the base prefix. A similar approach exists in Metalama.

Referencing introduced members in a template

When you introduce a member to a type, you will often want to access it from templates. There are three ways to do it:

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 of the approaches above offers you 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 are not visible in the meta code model exposed to the same aspect or aspect layer. To reference builders, you have to 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(this, new PropertyChangedEventArgs(propertyName));
11    }
12
13    public event PropertyChangedEventHandler? PropertyChanged;
14}

Referencing introduced members from source code

If you want the source code (not your aspect code) to reference declarations introduced by your aspect, the user of your aspect needs to make the target types partial. Without this keyword, the introduced declarations will not be visible at design time in syntax completion, and the IDE will report errors. Note that the compiler will not complain because Metalama replaces the compiler, but the IDE will because it does not know about Metalama, and therefore your aspect has to follow the rules of the C# compiler. However inconvenient it may be, there is nothing you as an aspect author, or us as the authors of Metalama, can do.

If the user does not add the partial keyword, Metalama will report a warning and offer a code fix.

Note

In test projects built using Metalama.Testing.AspectTesting, the Metalama compiler is not activated. Therefore, the source code of test projects cannot reference introduced declarations. Since the present documentation relies on Metalama.Testing.AspectTesting for all examples, we cannot include an example here.