Open sandboxFocusImprove this doc

Memento example, step 2: supporting type inheritance

In this second article, we will see how to modify our aspect to support type inheritance.

Strategizing

As always, we need to start reasoning and make some decisions about the implementation strategy before jumping into code.

We take the following approach:

  • Each originator class will still have its own memento class, and these memento classes will inherit from each other. So if Fish derives from FishtankArtifact, then Fish.Memento will derive from FishtankArtifact.Memento. Therefore, memento classes will be protected and not private. Each memento class will be responsible for its own properties, not for the properties of the base class.
  • Each RestoreMemento will be responsible only for the fields and properties of the current class and will call the base implementation to cope with properties of the base class.

Result

When we are done with the aspect, it will transform code as follows.

Here is a base class:

Source Code


1using Metalama.Patterns.Observability;
2
3[Memento]
4[Observable]
5public partial class FishtankArtifact
6{



















7    public string? Name { get; set; }


8








9    public DateTime DateAdded { get; set; }



10}
Transformed Code
1using System;
2using System.ComponentModel;
3using Metalama.Patterns.Observability;
4
5[Memento]
6[Observable]
7public partial class FishtankArtifact
8: INotifyPropertyChanged, IMementoable
9{
10private string? _name;
11
12    public string? Name { get { return this._name; } set { if (!object.ReferenceEquals(value, this._name)) { this._name = value; this.OnPropertyChanged("Name"); } } }
13    private DateTime _dateAdded;
14
15    public DateTime DateAdded { get { return this._dateAdded; } set { if (this._dateAdded != value) { this._dateAdded = value; this.OnPropertyChanged("DateAdded"); } } }
16    protected virtual void OnPropertyChanged(string propertyName)
17    {
18        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
19    }
20    public virtual void RestoreMemento(IMemento memento)
21    {
22        var typedMemento = (Memento)memento;
23        this.Name = ((Memento)typedMemento).Name;
24        this.DateAdded = ((Memento)typedMemento).DateAdded;
25    }
26    public virtual IMemento SaveToMemento()
27    {
28        return new Memento(this);
29    }
30    public event PropertyChangedEventHandler? PropertyChanged;
31
32    protected class Memento : IMemento
33    {
34        public Memento(FishtankArtifact originator)
35        {
36            this.Originator = originator;
37            this.Name = originator.Name;
38            this.DateAdded = originator.DateAdded;
39        }
40        public DateTime DateAdded { get; }
41        public string? Name { get; }
42        public IMementoable? Originator { get; }
43    }
44}

Here is a derived class:

Source Code


1public partial class Fish : FishtankArtifact
2{























3    public string? Species { get; set; }

4}
Transformed Code
1using System;
2
3public partial class Fish : FishtankArtifact
4{
5private string? _species;
6
7    public string? Species { get { return this._species; } set { if (!object.ReferenceEquals(value, this._species)) { this._species = value; this.OnPropertyChanged("Species"); } } }
8    protected override void OnPropertyChanged(string propertyName)
9    {
10        base.OnPropertyChanged(propertyName);
11    }
12    public override void RestoreMemento(IMemento memento)
13    {
14        base.RestoreMemento(memento);
15        var typedMemento = (Memento)memento;
16        this.Species = ((Memento)typedMemento).Species;
17    }
18    public override IMemento SaveToMemento()
19    {
20        return new Memento(this);
21    }
22    protected new class Memento : FishtankArtifact.Memento, IMemento
23    {
24        public Memento(Fish originator) : base(originator)
25        {
26            this.Species = originator.Species;
27        }
28        public string? Species { get; }
29    }
30}

Step 1. Mark the aspect as inheritable

We certainly want our [Memento] aspect to automatically apply to derived classes when we add it to a base class. We achieve this by adding the [Inheritable] attribute to the aspect class.

7[Inheritable]
8public sealed class MementoAttribute : TypeAspect
9

For details about aspect inheritance, see Applying aspects to derived types.

Step 2. Validating the base type

If the base type already implements IMementoable, we need to check that the implementation fulfills our expectations. Indeed, it is possible that IMementoable is implemented manually. The following rules must be respected:

  • There must be a Memento nested type in the base type.
  • This nested type must be protected.
  • This nested type must have a public or protected constructor accepting the base type as its only argument.

When doing any sort of validation, the first step is to define the errors we will use.

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4
5namespace DefaultNamespace;
6
7[CompileTime]
8internal static class DiagnosticDefinitions
9{
10    public static DiagnosticDefinition<INamedType> BaseTypeHasNoMementoType
11        = new(
12            "MEMENTO01",
13            Severity.Error,
14            "The base type '{0}' does not have a 'Memento' nested type." );
15
16    public static DiagnosticDefinition<INamedType> MementoTypeMustBeProtected
17        = new(
18            "MEMENTO02",
19            Severity.Error,
20            "The type '{0}' must be protected." );
21
22    public static DiagnosticDefinition<INamedType> MementoTypeMustNotBeSealed
23        = new(
24            "MEMENTO03",
25            Severity.Error,
26            "The type '{0}' must be not be sealed." );
27
28    public static DiagnosticDefinition<(INamedType MementoType, IType ParameterType)>
29        MementoTypeMustHaveConstructor
30            = new(
31                "MEMENTO04",
32                Severity.Error,
33                "The type '{0}' must have a constructor with a single parameter of type '{1}'." );
34
35    public static DiagnosticDefinition<IConstructor> MementoConstructorMustBePublicOrProtected
36        = new(
37            "MEMENTO05",
38            Severity.Error,
39            "The constructor '{0}' must be public or protected." );
40}

We can then validate the code.

28var isBaseMementotable = builder.Target.BaseType?.IsConvertibleTo( typeof(IMementoable) ) == true;
29
30INamedType? baseMementoType;
31IConstructor? baseMementoConstructor;
32
33if ( isBaseMementotable )
34{
35    var baseTypeDefinition = builder.Target.BaseType!.Definition;
36
37    baseMementoType = baseTypeDefinition.Types.OfName( "Memento" )
38        .SingleOrDefault();
39
40    if ( baseMementoType == null )
41    {
42        builder.Diagnostics.Report(
43            DiagnosticDefinitions.BaseTypeHasNoMementoType.WithArguments( baseTypeDefinition ) );
44
45        builder.SkipAspect();
46
47        return;
48    }
49
50    if ( baseMementoType.Accessibility !=
51         Metalama.Framework.Code.Accessibility.Protected )
52    {
53        builder.Diagnostics.Report(
54            DiagnosticDefinitions.MementoTypeMustBeProtected.WithArguments( baseMementoType ) );
55
56        builder.SkipAspect();
57
58        return;
59    }
60
61    if ( baseMementoType.IsSealed )
62    {
63        builder.Diagnostics.Report(
64            DiagnosticDefinitions.MementoTypeMustNotBeSealed.WithArguments( baseMementoType ) );
65
66        builder.SkipAspect();
67
68        return;
69    }
70
71    baseMementoConstructor = baseMementoType.Constructors
72        .FirstOrDefault(
73            c => c.Parameters.Count == 1 &&
74                 c.Parameters[0].Type.Equals( baseTypeDefinition ) );
75
76    if ( baseMementoConstructor == null )
77    {
78        builder.Diagnostics.Report(
79            DiagnosticDefinitions.MementoTypeMustHaveConstructor
80                .WithArguments( (baseMementoType, baseTypeDefinition) ) );
81
82        builder.SkipAspect();
83
84        return;
85    }
86
87    if ( baseMementoConstructor.Accessibility is not (Metalama.Framework.Code.Accessibility
88            .Protected or Metalama.Framework.Code.Accessibility.Public) )
89    {
90        builder.Diagnostics.Report(
91            DiagnosticDefinitions.MementoConstructorMustBePublicOrProtected
92                .WithArguments( baseMementoConstructor ) );
93
94        builder.SkipAspect();
95
96        return;
97    }
98}
99else
100{
101    baseMementoType = null;
102    baseMementoConstructor = null;
103}
104

For details regarding error reporting, see Reporting and suppressing diagnostics.

Step 2. Specifying the OverrideAction

By default, advising methods such as IntroduceClass or IntroduceMethod will fail if the same member already exists in the current or base type. To specify how the advising method should behave in this case, we must supply an OverrideStrategy to the whenExists parameter. The default value is Fail. We must change it to Ignore, Override, or New:

  • When using IntroduceClass to introduce the Memento nested class, we use New.
  • When using IntroduceMethod to introduce SaveToMemento or RestoreMemento, we use Override.
  • When using ImplementInterface to implement IMemento or IMementoable, we use Ignore.

Step 3. Setting the base type and constructor of the Memento type

Now that we know if there is a valid base type, we can modify the logic that introduces the nested class and set the BaseType property.

108// Introduce a new private nested class called Memento.
109var mementoType =
110    builder.IntroduceClass(
111        "Memento",
112        whenExists: OverrideStrategy.New,
113        buildType: b =>
114        {
115            b.Accessibility = Metalama.Framework.Code.Accessibility.Protected;
116            b.BaseType = baseMementoType;
117        } );
118

If we have a base class, we must also instruct the introduced constructor to call the base constructor. This is done by setting the InitializerKind property. We then call the AddInitializerArgument method and pass the IParameterBuilder returned by AddParameter.

162// Add a constructor to the Memento class that records the state of the originator.
163mementoType.IntroduceConstructor(
164    nameof(this.MementoConstructorTemplate),
165    buildConstructor: b =>
166    {
167        var parameter = b.AddParameter( "originator", builder.Target );
168
169        if ( baseMementoConstructor != null )
170        {
171            b.InitializerKind = ConstructorInitializerKind.Base;
172            b.AddInitializerArgument( parameter );
173        }
174    } );
175

Step 4. Calling the base implementation from RestoreMemento

Finally, we must edit the RestoreMemento template to ensure it calls the base method if it exists. This can be done by simply calling meta.Proceed(). If a base method exists, it will call it. Otherwise, this call will be ignored.

237[Template]
238public void RestoreMemento( IMemento memento )
239{
240    var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
241
242    // Call the base method if any.
243    meta.Proceed();
244
245    var typedMemento = meta.Cast( buildAspectInfo.MementoType, memento );
246
247    // Set fields of this instance to the values stored in the Memento.
248    foreach ( var pair in buildAspectInfo.PropertyMap )
249    {
250        pair.Key.Value = pair.Value.With( (IExpression) typedMemento ).Value;
251    }
252}
253

Complete aspect

Here is the MementoAttribute, now supporting class inheritance.

1using DefaultNamespace;
2using Metalama.Framework.Advising;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Code;
5
6// 
7[Inheritable]
8public sealed class MementoAttribute : TypeAspect
9
10// 
11{
12    [CompileTime]
13    private record BuildAspectInfo(
14
15        // The newly introduced Memento type.
16        INamedType MementoType,
17
18        // Mapping from fields or properties in the Originator to the corresponding property
19        // in the Memento type.
20        Dictionary<IFieldOrProperty, IProperty> PropertyMap,
21
22        // The Originator property in the new Memento type.
23        IProperty? OriginatorProperty );
24
25    public override void BuildAspect( IAspectBuilder<INamedType> builder )
26    {
27        // 
28        var isBaseMementotable = builder.Target.BaseType?.IsConvertibleTo( typeof(IMementoable) ) == true;
29
30        INamedType? baseMementoType;
31        IConstructor? baseMementoConstructor;
32
33        if ( isBaseMementotable )
34        {
35            var baseTypeDefinition = builder.Target.BaseType!.Definition;
36
37            baseMementoType = baseTypeDefinition.Types.OfName( "Memento" )
38                .SingleOrDefault();
39
40            if ( baseMementoType == null )
41            {
42                builder.Diagnostics.Report(
43                    DiagnosticDefinitions.BaseTypeHasNoMementoType.WithArguments( baseTypeDefinition ) );
44
45                builder.SkipAspect();
46
47                return;
48            }
49
50            if ( baseMementoType.Accessibility !=
51                 Metalama.Framework.Code.Accessibility.Protected )
52            {
53                builder.Diagnostics.Report(
54                    DiagnosticDefinitions.MementoTypeMustBeProtected.WithArguments( baseMementoType ) );
55
56                builder.SkipAspect();
57
58                return;
59            }
60
61            if ( baseMementoType.IsSealed )
62            {
63                builder.Diagnostics.Report(
64                    DiagnosticDefinitions.MementoTypeMustNotBeSealed.WithArguments( baseMementoType ) );
65
66                builder.SkipAspect();
67
68                return;
69            }
70
71            baseMementoConstructor = baseMementoType.Constructors
72                .FirstOrDefault(
73                    c => c.Parameters.Count == 1 &&
74                         c.Parameters[0].Type.Equals( baseTypeDefinition ) );
75
76            if ( baseMementoConstructor == null )
77            {
78                builder.Diagnostics.Report(
79                    DiagnosticDefinitions.MementoTypeMustHaveConstructor
80                        .WithArguments( (baseMementoType, baseTypeDefinition) ) );
81
82                builder.SkipAspect();
83
84                return;
85            }
86
87            if ( baseMementoConstructor.Accessibility is not (Metalama.Framework.Code.Accessibility
88                    .Protected or Metalama.Framework.Code.Accessibility.Public) )
89            {
90                builder.Diagnostics.Report(
91                    DiagnosticDefinitions.MementoConstructorMustBePublicOrProtected
92                        .WithArguments( baseMementoConstructor ) );
93
94                builder.SkipAspect();
95
96                return;
97            }
98        }
99        else
100        {
101            baseMementoType = null;
102            baseMementoConstructor = null;
103        }
104
105        // 
106
107        // 
108        // Introduce a new private nested class called Memento.
109        var mementoType =
110            builder.IntroduceClass(
111                "Memento",
112                whenExists: OverrideStrategy.New,
113                buildType: b =>
114                {
115                    b.Accessibility = Metalama.Framework.Code.Accessibility.Protected;
116                    b.BaseType = baseMementoType;
117                } );
118
119        // 
120
121        // 
122        var originatorFieldsAndProperties = builder.Target.FieldsAndProperties
123            .Where(
124                p => p is
125                {
126                    IsStatic: false,
127                    IsAutoPropertyOrField: true,
128                    IsImplicitlyDeclared: false,
129                    Writeability: Writeability.All
130                } )
131            .Where(
132                p =>
133                    !p.Attributes.OfAttributeType( typeof(MementoIgnoreAttribute) )
134                        .Any() );
135
136        // 
137
138        // 
139        // Introduce data properties to the Memento class for each field of the target class.
140        var propertyMap = new Dictionary<IFieldOrProperty, IProperty>();
141
142        foreach ( var fieldOrProperty in originatorFieldsAndProperties )
143        {
144            var introducedField = mementoType.IntroduceProperty(
145                nameof(this.MementoProperty),
146                buildProperty: b =>
147                {
148                    var trimmedName = fieldOrProperty.Name.TrimStart( '_' );
149
150                    b.Name = trimmedName.Substring( 0, 1 ).ToUpperInvariant() +
151                             trimmedName.Substring( 1 );
152
153                    b.Type = fieldOrProperty.Type;
154                } );
155
156            propertyMap.Add( fieldOrProperty, introducedField.Declaration );
157        }
158
159        // 
160
161        // 
162        // Add a constructor to the Memento class that records the state of the originator.
163        mementoType.IntroduceConstructor(
164            nameof(this.MementoConstructorTemplate),
165            buildConstructor: b =>
166            {
167                var parameter = b.AddParameter( "originator", builder.Target );
168
169                if ( baseMementoConstructor != null )
170                {
171                    b.InitializerKind = ConstructorInitializerKind.Base;
172                    b.AddInitializerArgument( parameter );
173                }
174            } );
175
176        // 
177
178        // 
179        // Implement the IMemento interface on the Memento class and add its members.   
180        mementoType.ImplementInterface(
181            typeof(IMemento),
182            whenExists: OverrideStrategy.Ignore );
183
184        var introducePropertyResult = mementoType.IntroduceProperty(
185            nameof(this.Originator),
186            whenExists: OverrideStrategy.Ignore );
187
188        var originatorProperty = introducePropertyResult.Outcome == AdviceOutcome.Default
189            ? introducePropertyResult.Declaration
190            : null;
191
192        // 
193
194        // Implement the rest of the IOriginator interface and its members.
195        builder.ImplementInterface( typeof(IMementoable), OverrideStrategy.Ignore );
196
197        builder.IntroduceMethod(
198            nameof(this.SaveToMemento),
199            whenExists: OverrideStrategy.Override,
200            buildMethod: m => m.IsVirtual = !builder.Target.IsSealed,
201            args: new { mementoType = mementoType.Declaration } );
202
203        builder.IntroduceMethod(
204            nameof(this.RestoreMemento),
205            buildMethod: m => m.IsVirtual = !builder.Target.IsSealed,
206            whenExists: OverrideStrategy.Override );
207
208        // Pass the state to the templates.
209        // 
210        builder.Tags = new BuildAspectInfo(
211            mementoType.Declaration,
212            propertyMap,
213            originatorProperty );
214
215        // 
216    }
217
218    [Template]
219    public object? MementoProperty { get; }
220
221    [Template]
222    public IMementoable? Originator { get; }
223
224    [Template]
225    public IMemento SaveToMemento()
226    {
227        // 
228        var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
229
230        // 
231
232        // Invoke the constructor of the Memento class and pass this object as the originator.
233        return buildAspectInfo.MementoType.Constructors.Single()
234            .Invoke( (IExpression) meta.This )!;
235    }
236
237    [Template]
238    public void RestoreMemento( IMemento memento )
239    {
240        var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
241
242        // Call the base method if any.
243        meta.Proceed();
244
245        var typedMemento = meta.Cast( buildAspectInfo.MementoType, memento );
246
247        // Set fields of this instance to the values stored in the Memento.
248        foreach ( var pair in buildAspectInfo.PropertyMap )
249        {
250            pair.Key.Value = pair.Value.With( (IExpression) typedMemento ).Value;
251        }
252    }
253
254    // 
255    [Template]
256    public void MementoConstructorTemplate()
257    {
258        var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
259
260        // Set the originator property and the data properties of the Memento.
261        if ( buildAspectInfo.OriginatorProperty != null )
262        {
263            buildAspectInfo.OriginatorProperty.Value = meta.Target.Parameters[0];
264        }
265        else
266        {
267            // We are in a derived type and there is no need to assign the property.
268        }
269
270        foreach ( var pair in buildAspectInfo.PropertyMap )
271        {
272            pair.Value.Value = pair.Key.With( meta.Target.Parameters[0] ).Value;
273        }
274    }
275
276    // 
277}