Open sandboxFocusImprove this doc

Implementing interfaces

Some aspects require modifying the target type to implement a new interface. This requires the programmatic advising API.

Step 1. Call AdviserExtensions.ImplementInterface

Within your implementation of the BuildAspect method, invoke the ImplementInterface method.

You might need to pass a value to the OverrideStrategy parameter to handle the situation where the target type, or any of its ancestors, already implements the interface. The most common behavior is OverrideStrategy.Ignore, but the default value is OverrideStrategy.Fail, consistent with other advice kinds.

Note

Unlike PostSharp, Metalama doesn't require the aspect class to implement the introduced interface.

Step 2, Option A. Add interface members declaratively

The next step is to ensure that the aspect class generates all interface members. You can do this declaratively or programmatically, and add implicit or explicit implementations.

Note

The ImplementInterface method doesn't verify if the aspect generates all required members. If your aspect fails to introduce a member, the C# compiler will report errors.

Let's start with the declarative approach.

Implement all interface members in the aspect and annotate them with the [InterfaceMember] custom attribute. This attribute instructs Metalama to introduce the member to the target class, but only if the ImplementInterface succeeds. If the advice is ignored because the type already implements the interface and OverrideStrategy.Ignore has been used, the member will not be introduced to the target type.

By default, Metalama creates an implicit (public) implementation. You can use the IsExplicit property to specify that an explicit implementation must be created instead of a public method.

Note

Using the [Introduce] also works but isn't recommended in this case because this approach ignores the result of the ImplementInterface method.

Example: IDisposable

This example shows an aspect that introduces the IDisposable interface. The implementation of the Dispose method disposes of all fields or properties that implement the IDisposable interface.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System;
4using System.Linq;
5
6namespace Doc.Disposable;
7
8internal class DisposableAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11    {
12        builder.ImplementInterface(
13            typeof(IDisposable),
14            whenExists: OverrideStrategy.Ignore );
15    }
16
17    // Introduces a the `Dispose(bool)` protected method, which is NOT an interface member.
18    [Introduce( Name = "Dispose", IsVirtual = true, WhenExists = OverrideStrategy.Override )]
19    protected void DisposeImpl( bool disposing )
20    {
21        // Call the base method, if any.
22        meta.Proceed();
23
24        var disposableFields = meta.Target.Type.FieldsAndProperties
25            .Where( x => x.Type.IsConvertibleTo( typeof(IDisposable) )
26                         && x.IsAutoPropertyOrField == true );
27
28        // Disposes the current field or property.
29        foreach ( var field in disposableFields )
30        {
31            if ( field.Type.IsNullable == false )
32            {
33                field.Value?.Dispose();
34            }
35            else
36            {
37                field.Value!.Dispose();
38            }
39        }
40    }
41
42    // Implementation of IDisposable.Dispose.
43    [InterfaceMember]
44    public void Dispose()
45    {
46        meta.This.Dispose( true );
47    }
48}
Source Code
1using System.IO;
2using System.Threading;

3
4#pragma warning disable CA1001 // Types that own disposable fields should be disposable
5
6namespace Doc.Disposable;
7
8[Disposable]
9internal class Foo
10{
11    private CancellationTokenSource _cancellationTokenSource = new();
12}
13




14[Disposable]
15internal class Bar : Foo






16{
17    private MemoryStream _stream = new();
18}
Transformed Code
1using System;
2using System.IO;
3using System.Threading;
4
5#pragma warning disable CA1001 // Types that own disposable fields should be disposable
6
7namespace Doc.Disposable;
8
9[Disposable]
10internal class Foo : IDisposable
11{
12    private CancellationTokenSource _cancellationTokenSource = new();
13
14    public void Dispose()
15    {
16        this.Dispose(true);
17    }
18
19    protected virtual void Dispose(bool disposing)
20    {
21        _cancellationTokenSource?.Dispose();
22    }
23}
24
25[Disposable]
26internal class Bar : Foo
27{
28    private MemoryStream _stream = new();
29
30    protected override void Dispose(bool disposing)
31    {
32        base.Dispose(disposing);
33        _stream?.Dispose();
34    }
35}

Example: Deep cloning

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Code.SyntaxBuilders;
5using System;
6using System.Linq;
7
8namespace Doc.DeepClone;
9
10[Inheritable]
11public class DeepCloneAttribute : TypeAspect
12{
13    public override void BuildAspect( IAspectBuilder<INamedType> builder )
14    {
15        builder.IntroduceMethod(
16            nameof(this.CloneImpl),
17            whenExists: OverrideStrategy.Override,
18            buildMethod: t =>
19            {
20                t.Name = "Clone";
21                t.ReturnType = builder.Target;
22            } );
23
24        builder.ImplementInterface(
25            typeof(ICloneable),
26            whenExists: OverrideStrategy.Ignore );
27    }
28
29    [Template( IsVirtual = true )]
30    public virtual dynamic CloneImpl()
31    {
32        // This compile-time variable will receive the expression representing the base call.
33        // If we have a public Clone method, we will use it (this is the chaining pattern). Otherwise,
34        // we will call MemberwiseClone (this is the initialization of the pattern).
35        IExpression baseCall;
36
37        if ( meta.Target.Method.IsOverride )
38        {
39            baseCall = meta.Base.Clone();
40        }
41        else
42        {
43            baseCall = meta.Base.MemberwiseClone();
44        }
45
46        // Define a local variable of the same type as the target type.
47        var cloneVariable = meta.DefineLocalVariable(
48            "clone",
49            baseCall.CastTo( meta.Target.Type ) );
50
51        // Select clonable fields.
52        var clonableFields =
53            meta.Target.Type.FieldsAndProperties.Where( f => f.IsAutoPropertyOrField == true &&
54                                                             ((f.Type.IsConvertibleTo(
55                                                                   typeof(ICloneable) )
56                                                               && f.Type.SpecialType
57                                                               != SpecialType.String)
58                                                              ||
59                                                              (f.Type is INamedType
60                                                               {
61                                                                   BelongsToCurrentProject: true
62                                                               } fieldNamedType &&
63                                                               fieldNamedType.Enhancements()
64                                                                   .HasAspect<
65                                                                       DeepCloneAttribute>())) );
66
67        foreach ( var field in clonableFields )
68        {
69            // Check if we have a public method 'Clone()' for the type of the field.
70            var fieldType = (INamedType) field.Type;
71            var cloneMethod = fieldType.Methods.OfExactSignature( "Clone", Array.Empty<IType>() );
72
73            IExpression callClone;
74
75            if ( cloneMethod is { Accessibility: Accessibility.Public } ||
76                 fieldType.Enhancements().HasAspect<DeepCloneAttribute>() )
77            {
78                // If yes, call the method without a cast.
79                callClone = field.Value?.Clone()!;
80            }
81            else
82            {
83                // If no, explicitly cast to the interface.
84                callClone = ExpressionFactory.Capture( ((ICloneable?) field.Value)?.Clone()! );
85            }
86
87            if ( cloneMethod == null
88                 || !cloneMethod.ReturnType.ToNullable().IsConvertibleTo( fieldType ) )
89            {
90                // If necessary, cast the return value of Clone to the field type.
91                callClone = callClone.CastTo( fieldType );
92            }
93
94            // Finally, set the field value.
95            field.WithObject( cloneVariable ).Value = callClone.Value;
96        }
97
98        return cloneVariable.Value!;
99    }
100
101    [InterfaceMember( IsExplicit = true )]
102    private object Clone()
103    {
104        return meta.This.Clone();
105    }
106}
Source Code
1using System;
2
3namespace Doc.DeepClone;
4
5internal class ManuallyCloneable : ICloneable
6{
7    public object Clone()
8    {
9        return new ManuallyCloneable();
10    }
11}
12
13[DeepClone]
14internal class AutomaticallyCloneable
15{
16    private int _a;
17    private ManuallyCloneable? _b;
18    private AutomaticallyCloneable? _c;
19}
20







21internal class DerivedCloneable : AutomaticallyCloneable
22{






23    private string? _d;
24}
Transformed Code
1using System;
2
3namespace Doc.DeepClone;
4
5internal class ManuallyCloneable : ICloneable
6{
7    public object Clone()
8    {
9        return new ManuallyCloneable();
10    }
11}
12
13[DeepClone]
14internal class AutomaticallyCloneable : ICloneable
15{
16    private int _a;
17    private ManuallyCloneable? _b;
18    private AutomaticallyCloneable? _c;
19
20    public virtual AutomaticallyCloneable Clone()
21    {
22        var clone = (AutomaticallyCloneable)this.MemberwiseClone();
23        clone._b = (ManuallyCloneable?)_b?.Clone();
24        clone._c = (_c?.Clone());
25        return clone;
26    }
27
28    object ICloneable.Clone()
29    {
30        return Clone();
31    }
32}
33
34internal class DerivedCloneable : AutomaticallyCloneable
35{
36    private string? _d;
37
38    public override DerivedCloneable Clone()
39    {
40        var clone = (DerivedCloneable)base.Clone();
41        return clone;
42    }
43}

Step 2, Option B. Add interface members programmatically

Use this approach instead of or with the declarative approach when:

  • The introduced interface is unknown to the aspect's author, for example, when the aspect's user can dynamically specify it.
  • Introducing a generic interface by using generic templates (see Template parameters and type parameters).

To programmatically add interface members, use one of the Introduce methods of the AdviserExtensions class, explained in Introducing members. Ensure these members are public.

To add explicit implementations instead of public members, use the ExplicitMembers property of the IImplementInterfaceAdviceResult returned by the ImplementInterface method, and call any of its Introduce methods.

Implementing explicit members dynamically

When the interface to implement is unknown at design time, or when you need to generate explicit implementations programmatically, use the result of the ImplementInterface method.

The ExplicitMembers property provides an adviser for introducing explicit member implementations. Use its IntroduceMethod, IntroduceProperty, IntroduceEvent, or IntroduceIndexer methods to add implementations dynamically.

To iterate over interface members, access the Interfaces collection, which contains an IInterfaceImplementationResult for each interface (including base interfaces). Each result provides:

  • InterfaceType — the interface being implemented
  • Outcome — whether the interface was implemented or ignored
  • ExplicitMembers — an adviser for that specific interface

To get interface members for use in templates, retrieve them from the interface type:

var result = builder.ImplementInterface(typeof(IDisposable));
var disposeMethod = result.Interfaces[0].InterfaceType.Methods.Single(m => m.Name == "Dispose");

Example: Delegate interface

This example demonstrates dynamic explicit interface implementation. The DelegateInterface aspect implements an interface by delegating all member calls to a field.

1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.DelegateInterface;
8
9/// <summary>
10/// Implements an interface by delegating all members to a field of the same type.
11/// </summary>
12public class DelegateInterfaceAttribute : TypeAspect
13{
14    private readonly Type _interfaceType;
15    private readonly string _fieldName;
16
17    public DelegateInterfaceAttribute( Type interfaceType, string fieldName )
18    {
19        this._interfaceType = interfaceType;
20        this._fieldName = fieldName;
21    }
22
23    public override void BuildAspect( IAspectBuilder<INamedType> builder )
24    {
25        // Get the interface type.
26        var interfaceType = (INamedType) TypeFactory.GetType( this._interfaceType );
27
28        // Get the field to delegate to.
29        var field = builder.Target.Fields.Single( f => f.Name == this._fieldName );
30
31        // Implement the interface.
32        var implementResult = builder.ImplementInterface( interfaceType, OverrideStrategy.Ignore );
33
34        // Introduce explicit implementations for all methods.
35        foreach ( var method in interfaceType.Methods )
36        {
37            implementResult.ExplicitMembers.IntroduceMethod(
38                nameof(this.DelegateMethodTemplate),
39                buildMethod: m =>
40                {
41                    m.Name = method.Name;
42                    m.ReturnType = method.ReturnType;
43
44                    // Copy parameters.
45                    foreach ( var param in method.Parameters )
46                    {
47                        m.AddParameter( param.Name, param.Type, param.RefKind );
48                    }
49                },
50                args: new { field, method } );
51        }
52
53        // Introduce explicit implementations for all properties.
54        foreach ( var property in interfaceType.Properties )
55        {
56            implementResult.ExplicitMembers.IntroduceProperty(
57                property.Name,
58                property.GetMethod != null ? nameof(this.DelegateGetterTemplate) : null,
59                property.SetMethod != null ? nameof(this.DelegateSetterTemplate) : null,
60                buildProperty: p => p.Type = property.Type,
61                args: new { field, property } );
62        }
63    }
64
65    [Template]
66    public dynamic? DelegateMethodTemplate( IField field, IMethod method )
67    {
68        return method.WithObject( field ).Invoke( meta.Target.Parameters.Cast<IExpression>() );
69    }
70
71    [Template]
72    public dynamic? DelegateGetterTemplate( IField field, IProperty property )
73    {
74        return property.WithObject( field ).Value;
75    }
76
77    [Template]
78    public void DelegateSetterTemplate( IField field, IProperty property )
79    {
80        property.WithObject( field ).Value = meta.Target.Parameters[0].Value;
81    }
82}
83
Source Code
1namespace Doc.DelegateInterface;
2

3public interface IGreeter
4{
5    string Name { get; set; }
6
7    string Greet( string message );
8}
9
10internal class InternalGreeter : IGreeter
11{
12    public string Name { get; set; } = "World";
13
14    public string Greet( string message ) => $"{message}, {this.Name}!";
15}
16
17// The aspect implements IGreeter by delegating to the _greeter field.
18[DelegateInterface( typeof(IGreeter), "_greeter" )]
19public partial class MyService
20{
21    private readonly IGreeter _greeter = new InternalGreeter();
22}
23
Transformed Code
1namespace Doc.DelegateInterface;
2
3public interface IGreeter
4{
5    string Name { get; set; }
6
7    string Greet(string message);
8}
9
10internal class InternalGreeter : IGreeter
11{
12    public string Name { get; set; } = "World";
13
14    public string Greet(string message) => $"{message}, {this.Name}!";
15}
16
17// The aspect implements IGreeter by delegating to the _greeter field.
18[DelegateInterface(typeof(IGreeter), "_greeter")]
19public partial class MyService : IGreeter
20{
21    private readonly IGreeter _greeter = new InternalGreeter();
22
23    string IGreeter.Name
24    {
25        get
26        {
27            return _greeter.Name;
28        }
29        set
30        {
31            _greeter.Name = value;
32        }
33    }
34
35    string IGreeter.Greet(string message)
36    {
37        return _greeter.Greet(message);
38    }
39}
40

Referencing interface members in other templates

When introducing an interface member to the type, you often want to access it from templates. There are four ways to call an introduced method from another template:

Option 1. Access the aspect template member

Call the method directly on this. This is the simplest approach.

this.Dispose();

Option 2. Use meta.This and write dynamic code

Use meta.This when you need dynamic dispatch or late binding.

meta.This.Dispose();

Option 3. Use invokers

If you have an IMethod reference, use the invoker API to call it. For details, see Generating code based on the code model.

You can obtain the IMethod from:

  • The interface type using TypeFactory.GetType(typeof(IDisposable)).Methods
  • The advice result using builder.IntroduceMethod(...).Declaration
disposeMethod.Invoke();

Option 4. Cast to interface

Cast meta.This to the interface type. This is useful for explicit implementations and ensures the interface method is called.

((IDisposable) meta.This).Dispose();

Example: Four ways to call interface members

This example demonstrates all four approaches to call an introduced Dispose method from another template:

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using System;
4using System.Linq;
5
6namespace Doc.ImplementInterfaceFourWays;
7
8public class GenerateResetAttribute : TypeAspect
9{
10    public override void BuildAspect( IAspectBuilder<INamedType> builder )
11    {
12        // Implement IDisposable.
13        builder.ImplementInterface( typeof(IDisposable), OverrideStrategy.Ignore );
14    }
15
16    // Implements IDisposable.Dispose.
17    [InterfaceMember]
18    public void Dispose()
19    {
20        Console.WriteLine( "Disposing..." );
21    }
22
23     // Introduce a Reset method demonstrating four ways to call Dispose.
24    [Introduce]
25    public void Reset()
26    {
27        // Option 1: Access the aspect template member directly.
28        meta.InsertComment( "Option 1: this.Dispose()" );
29        this.Dispose();
30
31        // Option 2: Use meta.This for dynamic code.
32        meta.InsertComment( "Option 2: meta.This.Dispose()" );
33        meta.This.Dispose();
34
35        // Option 3: Use the invoker API on the interface method.
36        meta.InsertComment( "Option 3: disposeMethod.Invoke()" );
37        var disposableInterface = (INamedType) TypeFactory.GetType( typeof(IDisposable) );
38        var disposeMethod = disposableInterface.Methods.Single( m => m.Name == "Dispose" );
39        disposeMethod.Invoke();
40
41        // Option 4: Cast to interface (useful for explicit implementations).
42        meta.InsertComment( "Option 4: ((IDisposable)meta.This).Dispose()" );
43        ((IDisposable) meta.This).Dispose();
44    }
45}
46
Source Code
1namespace Doc.ImplementInterfaceFourWays;
2


3// The aspect introduces IDisposable and a Reset method.
4// Reset demonstrates four ways to call the introduced Dispose method.
5[GenerateReset]
6public partial class MyResource
7{
8}
9
Transformed Code
1using System;
2
3namespace Doc.ImplementInterfaceFourWays;
4
5// The aspect introduces IDisposable and a Reset method.
6// Reset demonstrates four ways to call the introduced Dispose method.
7[GenerateReset]
8public partial class MyResource : IDisposable
9{
10    public void Dispose()
11    {
12        Console.WriteLine("Disposing...");
13    }
14
15    public void Reset()
16    {
17        // Option 1: this.Dispose()
18        Dispose();
19        // Option 2: meta.This.Dispose()
20        this.Dispose();
21        // Option 3: disposeMethod.Invoke()
22        ((IDisposable)this).Dispose();
23        // Option 4: ((IDisposable)meta.This).Dispose()
24        ((IDisposable)this).Dispose();
25    }
26}
27