Open sandboxFocusImprove this doc

Clone example, step 3: allowing handmade customizations

In the previous articles, we built a Cloneable aspect that worked well with simple classes and one-to-one relationships. But what if we need to support external types for which we cannot add a Clone method, or one-to-many relationships, such as collection fields?

Ideally, we would build a pluggable cloning service for external types, as we did for caching key builders of external types (see Caching example, step 4: cache key for external types) and supply cloners for system collections. But before that, an even better strategy is to design an extension point that the aspect's users can use when our aspect has limitations. How can we allow the aspect's users to inject their custom logic?

We will let users add their custom logic after the aspect-generated logic by allowing them to supply a method with the following signature, where T is the current type:

private void CloneMembers(T clone)

The aspect will inject its logic before the user's implementation.

Let's see this pattern in action. In this new example, the Game class has a one-to-many relationship with the Player class. The cloning of the collection is implemented manually.

Source Code


1[Cloneable]
2internal class Game
3{

4    public List<Player> Players { get; private set; } = new();
5
6    [Child]
7    public GameSettings Settings { get; set; }










8
9    private void CloneMembers( Game clone ) => clone.Players = new List<Player>( this.Players );



10}
Transformed Code
1using System;
2
3[Cloneable]
4internal class Game
5: ICloneable
6{
7    public List<Player> Players { get; private set; } = new();
8
9    [Child]
10    public GameSettings Settings { get; set; }
11
12    private void CloneMembers( Game clone ) { clone.Settings = ((GameSettings)this.Settings?.Clone());
13        clone.Players = new List<Player>(this.Players);
14    }
15public virtual Game Clone()
16    {
17        var clone = (Game)this.MemberwiseClone();
18        this.CloneMembers(clone);
19        return clone;
20    }
21
22    object ICloneable.Clone()
23    {
24        return Clone();
25    }
26}
Source Code


1[Cloneable]
2internal class GameSettings
3{

4    public int Level { get; set; }
5
6    public string World { get; set; }













7}
Transformed Code
1using System;
2
3[Cloneable]
4internal class GameSettings
5: ICloneable
6{
7    public int Level { get; set; }
8
9    public string World { get; set; }
10public virtual GameSettings Clone()
11    {
12        var clone = (GameSettings)this.MemberwiseClone();
13        this.CloneMembers(clone);
14        return clone;
15    }
16    private void CloneMembers(GameSettings clone)
17    { }
18
19    object ICloneable.Clone()
20    {
21        return Clone();
22    }
23}

Aspect implementation

Here is the updated CloneableAttribute class:

1using JetBrains.Annotations;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Diagnostics;
5using Metalama.Framework.Project;
6
7[Inheritable]
8[EditorExperience( SuggestAsLiveTemplate = true )]
9public class CloneableAttribute : TypeAspect
10{
11    // 
12    private static readonly
13        DiagnosticDefinition<(DeclarationKind, IFieldOrProperty)>
14        _fieldOrPropertyCannotBeReadOnly =
15            new(
16                "CLONE01",
17                Severity.Error,
18                "The {0} '{1}' cannot be read-only because it is marked as a [Child]." );
19
20    private static readonly DiagnosticDefinition<(DeclarationKind, IFieldOrProperty, IType)>
21        _missingCloneMethod =
22            new(
23                "CLONE02",
24                Severity.Error,
25                "The {0} '{1}' cannot be a [Child] because its type '{2}' does not have a 'Clone' parameterless method." );
26
27    private static readonly DiagnosticDefinition<IMethod> _cloneMethodMustBePublic =
28        new(
29            "CLONE03",
30            Severity.Error,
31            "The '{0}' method must be public or internal." );
32
33    private static readonly DiagnosticDefinition<IProperty> _childPropertyMustBeAutomatic =
34        new(
35            "CLONE04",
36            Severity.Error,
37            "The property '{0}' cannot be a [Child] because is not an automatic property." );
38
39    // 
40
41    public override void BuildAspect( IAspectBuilder<INamedType> builder )
42    {
43        // Verify child fields and properties.
44        if ( !this.VerifyFieldsAndProperties( builder ) )
45        {
46            builder.SkipAspect();
47
48            return;
49        }
50
51        // Introduce the Clone method.
52        builder.IntroduceMethod(
53            nameof(this.CloneImpl),
54            whenExists: OverrideStrategy.Override,
55            args: new { T = builder.Target },
56            buildMethod: m =>
57            {
58                m.Name = "Clone";
59                m.ReturnType = builder.Target;
60            } );
61
62// 
63        builder.IntroduceMethod(
64            nameof(this.CloneMembers),
65            whenExists: OverrideStrategy.Override,
66            args: new { T = builder.Target } );
67
68// 
69
70        // Implement the ICloneable interface.
71        builder.ImplementInterface(
72            typeof(ICloneable),
73            OverrideStrategy.Ignore );
74    }
75
76    private bool VerifyFieldsAndProperties( IAspectBuilder<INamedType> builder )
77    {
78        var success = true;
79
80        // Verify that child fields are valid.
81        foreach ( var fieldOrProperty in GetCloneableFieldsOrProperties( builder.Target ) )
82        {
83            // The field or property must be writable.
84            if ( fieldOrProperty.Writeability != Writeability.All )
85            {
86                builder.Diagnostics.Report(
87                    _fieldOrPropertyCannotBeReadOnly.WithArguments(
88                        (
89                            fieldOrProperty.DeclarationKind,
90                            fieldOrProperty) ),
91                    fieldOrProperty );
92
93                success = false;
94            }
95
96            // If it is a field, it must be an automatic property.
97            if ( fieldOrProperty is IProperty { IsAutoPropertyOrField: false } property )
98            {
99                builder.Diagnostics.Report(
100                    _childPropertyMustBeAutomatic.WithArguments( property ),
101                    property );
102
103                success = false;
104            }
105
106            // The type of the field must be cloneable.
107            void ReportMissingMethod()
108            {
109                builder.Diagnostics.Report(
110                    _missingCloneMethod.WithArguments(
111                        (fieldOrProperty.DeclarationKind,
112                         fieldOrProperty,
113                         fieldOrProperty.Type) ),
114                    fieldOrProperty );
115            }
116
117            if ( fieldOrProperty.Type is not INamedType fieldType )
118            {
119                // The field type is an array, a pointer or another special type, which do not have a Clone method.
120                ReportMissingMethod();
121                success = false;
122            }
123            else
124            {
125                var cloneMethod = fieldType.AllMethods.OfName( "Clone" )
126                    .SingleOrDefault( p => p.Parameters.Count == 0 );
127
128                if ( cloneMethod == null )
129                {
130                    // There is no Clone method.
131                    // It may be implemented by an aspect, but we don't have access to aspects on other types
132                    // at design time.
133                    if ( !MetalamaExecutionContext.Current.ExecutionScenario.IsDesignTime )
134                    {
135                        if ( !fieldType.BelongsToCurrentProject ||
136                             !fieldType.Enhancements().HasAspect<CloneableAttribute>() )
137                        {
138                            ReportMissingMethod();
139                            success = false;
140                        }
141                    }
142                }
143                else if ( cloneMethod.Accessibility is not (Accessibility.Public
144                         or Accessibility.Internal) )
145                {
146                    // If we have a Clone method, it must be public.
147                    builder.Diagnostics.Report(
148                        _cloneMethodMustBePublic.WithArguments( cloneMethod ),
149                        fieldOrProperty );
150
151                    success = false;
152                }
153            }
154        }
155
156        return success;
157    }
158
159    private static IEnumerable<IFieldOrProperty> GetCloneableFieldsOrProperties( INamedType type )
160        => type.FieldsAndProperties.Where( f =>
161                                               f.Attributes.OfAttributeType( typeof(ChildAttribute) ).Any() );
162
163    [Template]
164    public virtual T CloneImpl<[CompileTime] T>()
165    {
166        // This compile-time variable will receive the expression representing the base call.
167        // If we have a public Clone method, we will use it (this is the chaining pattern). Otherwise,
168        // we will call MemberwiseClone (this is the initialization of the pattern).
169        IExpression baseCall;
170
171        if ( meta.Target.Method.IsOverride )
172        {
173            baseCall = (IExpression) meta.Base.Clone();
174        }
175        else
176        {
177            baseCall = (IExpression) meta.This.MemberwiseClone();
178        }
179
180        // Define a local variable of the same type as the target type.
181        var clone = (T) baseCall.Value!;
182
183        // Call CloneMembers, which may have a handwritten part.
184        meta.This.CloneMembers( clone );
185
186        return clone;
187    }
188
189    [Template]
190    private void CloneMembers<[CompileTime] T>( T clone )
191    {
192        // Select cloneable fields.
193        var cloneableFields = GetCloneableFieldsOrProperties( meta.Target.Type );
194
195        foreach ( var field in cloneableFields )
196        {
197            // Check if we have a public method 'Clone()' for the type of the field.
198            var fieldType = (INamedType) field.Type;
199
200            field.WithObject( clone ).Value = meta.Cast( fieldType, field.Value?.Clone() );
201        }
202
203        // Call the handwritten implementation, if any.
204        meta.Proceed();
205    }
206
207    [InterfaceMember( IsExplicit = true )]
208    [UsedImplicitly]
209    private object Clone() => meta.This.Clone();
210}

We added the following code in the BuildAspect method:

63builder.IntroduceMethod(
64    nameof(this.CloneMembers),
65    whenExists: OverrideStrategy.Override,
66    args: new { T = builder.Target } );
67

The template for the CloneMembers method is as follows:

189[Template]
190private void CloneMembers<[CompileTime] T>( T clone )
191{
192    // Select cloneable fields.
193    var cloneableFields = GetCloneableFieldsOrProperties( meta.Target.Type );
194
195    foreach ( var field in cloneableFields )
196    {
197        // Check if we have a public method 'Clone()' for the type of the field.
198        var fieldType = (INamedType) field.Type;
199
200        field.WithObject( clone ).Value = meta.Cast( fieldType, field.Value?.Clone() );
201    }
202
203    // Call the handwritten implementation, if any.
204    meta.Proceed();
205}
206

As you can see, we moved the logic that clones individual fields to this method. We call meta.Proceed() last, so hand-written code is executed after aspect-generated code and can fix whatever gap the aspect left.

Summary

We updated the aspect to add an extensibility mechanism allowing the user to implement scenarios that lack genuine support by the aspect. The problem with this approach is that users may easily forget that they have to supply a private void CloneMembers(T clone) method. To remedy this issue, we will provide them with suggestions in the code refactoring menu.