Certain aspects necessitate modifying the target type to implement a new interface. This can only be achieved by using 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 cope with 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 in PostSharp, it is not necessary in Metalama for the aspect class to implement the introduced interface.
Step 2, Option A. Add interface members to the aspect class, declaratively
The next step is to ensure that the aspect class generates all interface members. We can do this declaratively or programmatically and add implicit or explicit implementations.
Note
The ImplementInterface method does not 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, an implicit (public) implementation is created. 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 is not recommended in this case because this approach ignores the result of the ImplementInterface method.
Example: IDisposable
In the subsequent example, the aspect introduces the IDisposable
interface. The implementation of the Dispose
method disposes of all fields or properties that implement the IDisposable
interface.
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using System;
5using System.Linq;
6
7namespace Doc.Disposable;
8
9internal class DisposableAttribute : TypeAspect
10{
11 public override void BuildAspect( IAspectBuilder<INamedType> builder )
12 {
13 builder.ImplementInterface(
14 typeof(IDisposable),
15 whenExists: OverrideStrategy.Ignore );
16 }
17
18 // Introduces a the `Dispose(bool)` protected method, which is NOT an interface member.
19 [Introduce( Name = "Dispose", IsVirtual = true, WhenExists = OverrideStrategy.Override )]
20 protected void DisposeImpl( bool disposing )
21 {
22 // Call the base method, if any.
23 meta.Proceed();
24
25 var disposableFields = meta.Target.Type.FieldsAndProperties
26 .Where(
27 x => x.Type.IsConvertibleTo( typeof(IDisposable) )
28 && x.IsAutoPropertyOrField == true );
29
30 // Disposes the current field or property.
31 foreach ( var field in disposableFields )
32 {
33 field.Value?.Dispose();
34 }
35 }
36
37 // Implementation of IDisposable.Dispose.
38 [InterfaceMember]
39 public void Dispose()
40 {
41 meta.This.Dispose( true );
42 }
43}
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}
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(
54 f => f.IsAutoPropertyOrField == true &&
55 ((f.Type.IsConvertibleTo( typeof(ICloneable) )
56 && f.Type.SpecialType != SpecialType.String)
57 ||
58 (f.Type is INamedType { BelongsToCurrentProject: true } fieldNamedType &&
59 fieldNamedType.Enhancements().HasAspect<DeepCloneAttribute>())) );
60
61 foreach ( var field in clonableFields )
62 {
63 // Check if we have a public method 'Clone()' for the type of the field.
64 var fieldType = (INamedType) field.Type;
65 var cloneMethod = fieldType.Methods.OfExactSignature( "Clone", Array.Empty<IType>() );
66
67 IExpression callClone;
68
69 if ( cloneMethod is { Accessibility: Accessibility.Public } ||
70 fieldType.Enhancements().HasAspect<DeepCloneAttribute>() )
71 {
72 // If yes, call the method without a cast.
73 callClone = field.Value?.Clone()!;
74 }
75 else
76 {
77 // If no, explicitly cast to the interface.
78 callClone = ExpressionFactory.Capture( ((ICloneable?) field.Value)?.Clone()! );
79 }
80
81 if ( cloneMethod == null
82 || !cloneMethod.ReturnType.ToNullable().IsConvertibleTo( fieldType ) )
83 {
84 // If necessary, cast the return value of Clone to the field type.
85 callClone = callClone.CastTo( fieldType );
86 }
87
88 // Finally, set the field value.
89 field.With( cloneVariable ).Value = callClone.Value;
90 }
91
92 return cloneVariable.Value!;
93 }
94
95 [InterfaceMember( IsExplicit = true )]
96 private object Clone()
97 {
98 return meta.This.Clone();
99 }
100}
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}
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 to the aspect class, programmatically
This approach can be used instead or in complement to the declarative one.
It is useful in the following situations:
- when the introduced interface is unknown to the aspect's author, e.g., when it can be dynamically specified by the aspect's user;
- when introducing a generic interface thanks to the ability to use generic templates (see Template parameters and type parameters).
To programmatically add interface members, use one of the Introduce
methods of the AdviserExtensions class, as explained in Introducing members. Make sure that these members are public.
If instead of adding public members you need to add explicit implementations, use the ExplicitMembers property of the IImplementInterfaceAdviceResult returned by the ImplementInterface method, and call any of its Introduce
methods.
Referencing interface members in other templates
When introducing an interface member to the type, you often want to access it from templates. Unless the member is an explicit implementation, you have two options:
Option 1. Access the aspect template member
this.Dispose();
Option 2. Use meta.This
and write dynamic code
meta.This.Dispose();
Option 3. Use invokers
If the members have been added programmatically, you can use invoker APIs like IMethod.Invoke for methods or Value for properties.
Accessing explicit implementations
The following strategies can be employed to access explicit implementations:
Cast the instance to the interface and access the member:
((IDisposable)meta.This).Dispose();
Introduce a private method with the concrete method implementation, and call this private member both from the interface member and the templates.