Open sandboxFocusImprove this doc

Clone example, step 1: getting started

This article will create the first working version of the Cloneable aspect. Once it is done, it will implement the Deep Clone pattern as shown below:

Source Code


1[Cloneable]
2internal class Game
3{

4    public Player Player { get; set; }
5
6    [Child] public GameSettings Settings { get; set; }











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


1[Cloneable]
2internal class GameSettings
3{

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










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

Before we start writing the aspect, we must materialize in C# the concept of a child property. Conceptually, a child property is a property that points to a reference-type object that needs to be cloned when the parent object is cloned. Let's decide to mark such properties with the [Child] custom attribute:

1[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
2public sealed class ChildAttribute : Attribute
3{
4}

Aspect implementation

The whole aspect implementation is here:

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3
4[Inheritable]
5[EditorExperience(SuggestAsLiveTemplate = true)]
6public class CloneableAttribute : TypeAspect
7{
8    public override void BuildAspect(IAspectBuilder<INamedType> builder)
9    {
10        // 
11        builder.Advice.ImplementInterface(
12            builder.Target,
13            typeof(ICloneable),
14            OverrideStrategy.Ignore);
15        // 
16
17        // 
18        builder.Advice.IntroduceMethod(
19            builder.Target,
20            nameof(this.CloneImpl),
21            whenExists: OverrideStrategy.Override,
22            args: new { T = builder.Target },
23            buildMethod: m => m.Name = "Clone");
24        // 
25    }
26
27    [InterfaceMember(IsExplicit = true)]
28    private object Clone() => meta.This.Clone();
29
30    [Template]
31    public virtual T CloneImpl<[CompileTime] T>()
32    {
33        // This compile-time variable will receive the expression representing the base call.
34        // If we have a public Clone method, we will use it (this is the chaining pattern). Otherwise,
35        // we will call MemberwiseClone (this is the initialization of the pattern).
36        IExpression baseCall;
37
38        if (meta.Target.Method.IsOverride)
39        {
40            baseCall = (IExpression)meta.Base.Clone();
41        }
42        else
43        {
44            baseCall = (IExpression)meta.This.MemberwiseClone();
45        }
46
47        // Define a local variable of the same type as the target type.
48        var clone = (T)baseCall.Value!;
49
50        // Select cloneable fields.
51        var cloneableFields =
52            meta.Target.Type.FieldsAndProperties.Where(
53                f => f.Attributes.OfAttributeType(typeof(ChildAttribute)).Any());
54
55        foreach (var field in cloneableFields)
56        {
57            // Check if we have a public method 'Clone()' for the type of the field.
58            var fieldType = (INamedType)field.Type;
59
60            field.With(clone).Value = meta.Cast(fieldType, field.Value?.Clone());
61        }
62
63        return clone;
64    }
65}

The BuildAspect method is the entry point of the aspect.

You can clearly see two steps in this method. We will comment on them independently.

Implementing the interface

The first operation of BuildAspect is to add the ICloneable method to the current type using the ImplementInterface method.

11builder.Advice.ImplementInterface(
12    builder.Target,
13    typeof(ICloneable),
14    OverrideStrategy.Ignore);

If the type already implements the ICloneable method, we don't need to do anything, so we are specifying Ignore as the OverrideStrategy. The ImplementInterface method requires the aspect type to include all interface members and to annotate them with the [InterfaceMember] custom attribute.

Our interface implementation calls the public Clone method we will introduce in the type.

27[InterfaceMember(IsExplicit = true)]
28private object Clone() => meta.This.Clone();
29

For details, see Implementing interfaces.

Note that the code uses the expression meta.This, a compile-time expression that returns a dynamic value. Thanks to its dynamic nature, you can write any run-time expression on its right side. This code is not verified until all aspects have been executed, so you can call a method that does not exist yet. For details regarding these techniques, see Generating run-time code

Adding the public method

The second operation of BuildAspect is to introduce a method named Clone by invoking IntroduceMethod.

18builder.Advice.IntroduceMethod(
19    builder.Target,
20    nameof(this.CloneImpl),
21    whenExists: OverrideStrategy.Override,
22    args: new { T = builder.Target },
23    buildMethod: m => m.Name = "Clone");

We set the OverrideStrategy to Override, indicating that the method should be overridden if it already exists in the type. The invocation of IntroduceMethod is more complex than usual for two reasons:

  1. The template method cannot be named Clone because it would conflict with the other Clone method of this aspect, the template for the ICloneable.Clone method. Therefore, we name the template method CloneImpl and rename the introduced method using the delegate passed to the buildMethod parameter. Hence, the code buildMethod: m => m.Name = "Clone".

  2. The CloneImpl template, as we will see below, has a compile-time generic parameter T, where T represents the current type. We need to pass the value of the T parameter in our invocation to the IntroduceMethod method. We pass an anonymous type to the args parameter, with the property T set to its desired value: args: new { T = builder.Target }.

For details, see Introducing members.

Let's now examine the CloneImpl template:

30[Template]
31public virtual T CloneImpl<[CompileTime] T>()
32{
33    // This compile-time variable will receive the expression representing the base call.
34    // If we have a public Clone method, we will use it (this is the chaining pattern). Otherwise,
35    // we will call MemberwiseClone (this is the initialization of the pattern).
36    IExpression baseCall;
37
38    if (meta.Target.Method.IsOverride)
39    {
40        baseCall = (IExpression)meta.Base.Clone();
41    }
42    else
43    {
44        baseCall = (IExpression)meta.This.MemberwiseClone();
45    }
46
47    // Define a local variable of the same type as the target type.
48    var clone = (T)baseCall.Value!;
49
50    // Select cloneable fields.
51    var cloneableFields =
52        meta.Target.Type.FieldsAndProperties.Where(
53            f => f.Attributes.OfAttributeType(typeof(ChildAttribute)).Any());
54
55    foreach (var field in cloneableFields)
56    {
57        // Check if we have a public method 'Clone()' for the type of the field.
58        var fieldType = (INamedType)field.Type;
59
60        field.With(clone).Value = meta.Cast(fieldType, field.Value?.Clone());
61    }
62
63    return clone;
64}

The first half of the method generates the base call with two possibilities:

  • When the method is an override: var clone = (T) base.Clone();
  • Otherwise, when the base type is not deeply clonable: var clone = (T) this.MemberwiseClone();

MemberwiseClone is a standard method of the object class. It returns a shallow copy of the current object. Using the MemberwiseClone has many benefits:

  • It is faster than setting individual fields or properties in C#.
  • It works even when the base type is unaware of the Clone pattern.

meta.Base works similarly to the xref: Metalama.Framework.Aspects.meta.This?text=meta.This> we already used before. It returns a dynamic value, and anything you write on its right side becomes a run-time expression, i.e., C# code injected by the template. To convert this code into a compile-time IExpression object, we cast the dynamic expression into IExpression.

The second part of the CloneImpl template selects all fields and properties annotated with the [Child] attribute and generates code according to the pattern clone.Foo = (FooType?) this.Foo?.Clone(). Fields or properties are represented as compile-time objects by the IFieldOrProperty interface. The Value property operates the same kind of magic as meta.This or meta.Base above, i.e., a dynamic property that can be used in run-time code. By default, field.Value generates a reference to the field for the current instance (i.e. this.Foo). To get the field for a different instance ( e.g. clone.Foo), you must use With.

Summary

In this article, we have created a Cloneable aspect that performs deep cloning of an object by recursively calling the Clone method on child properties. However, we did not validate that the child objects actually have a Clone method or that child properties are not read-only. We will address this problem the following step.