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}
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 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:
- IntroduceMethod returning an IMethodBuilder
- IntroduceProperty returning an IPropertyBuilder
- IntroduceEvent returning an IEventBuilder
- IntroduceField returning an IFieldBuilder
- IntroduceConstructor returning an IConstructorBuilder
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}
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}
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
orIsAbstract
property of the[Template]
attribute. - Set the
IsPartial
orIsAbstract
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.
- 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.
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}
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 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}
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(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.