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
Fishderives fromFishtankArtifact, thenFish.Mementowill derive fromFishtankArtifact.Memento. Therefore, memento classes will beprotectedand notprivate. Each memento class will be responsible for its own properties, not for the properties of the base class. - Each
RestoreMementowill be responsible only for the fields and properties of the current class and will call thebaseimplementation 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:
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}
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:
1public partial class Fish : FishtankArtifact
2{
3 public string? Species { get; set; }
4}
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
9For 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
Mementonested 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( c => c.Parameters.Count == 1 &&
73 c.Parameters[0].Type.IsConvertibleTo( baseTypeDefinition ) );
74
75 if ( baseMementoConstructor == null )
76 {
77 builder.Diagnostics.Report(
78 DiagnosticDefinitions.MementoTypeMustHaveConstructor
79 .WithArguments( (baseMementoType, baseTypeDefinition) ) );
80
81 builder.SkipAspect();
82
83 return;
84 }
85
86 if ( baseMementoConstructor.Accessibility is not (Metalama.Framework.Code.Accessibility
87 .Protected or Metalama.Framework.Code.Accessibility.Public) )
88 {
89 builder.Diagnostics.Report(
90 DiagnosticDefinitions.MementoConstructorMustBePublicOrProtected
91 .WithArguments( baseMementoConstructor ) );
92
93 builder.SkipAspect();
94
95 return;
96 }
97}
98else
99{
100 baseMementoType = null;
101 baseMementoConstructor = null;
102}
103For 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
Mementonested class, we useNew. - When using IntroduceMethod to introduce
SaveToMementoorRestoreMemento, we useOverride. - When using ImplementInterface to implement
IMementoorIMementoable, we useIgnore.
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.
107// Introduce a new private nested class called Memento.
108var mementoType =
109 builder.IntroduceClass(
110 "Memento",
111 whenExists: OverrideStrategy.New,
112 buildType: b =>
113 {
114 b.Accessibility = Metalama.Framework.Code.Accessibility.Protected;
115 b.BaseType = baseMementoType;
116 } );
117If 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.
159// Add a constructor to the Memento class that records the state of the originator.
160mementoType.IntroduceConstructor(
161 nameof(this.MementoConstructorTemplate),
162 buildConstructor: b =>
163 {
164 var parameter = b.AddParameter( "originator", builder.Target );
165
166 if ( baseMementoConstructor != null )
167 {
168 b.InitializerKind = ConstructorInitializerKind.Base;
169 b.AddInitializerArgument( parameter );
170 }
171 } );
172Step 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.
234[Template]
235public void RestoreMemento( IMemento memento )
236{
237 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
238
239 // Call the base method if any.
240 meta.Proceed();
241
242 var typedMemento = meta.Cast( buildAspectInfo.MementoType, memento );
243
244 // Set fields of this instance to the values stored in the Memento.
245 foreach ( var pair in buildAspectInfo.PropertyMap )
246 {
247 pair.Key.Value = pair.Value.WithObject( (IExpression) typedMemento ).Value;
248 }
249}
250Complete 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( c => c.Parameters.Count == 1 &&
73 c.Parameters[0].Type.IsConvertibleTo( baseTypeDefinition ) );
74
75 if ( baseMementoConstructor == null )
76 {
77 builder.Diagnostics.Report(
78 DiagnosticDefinitions.MementoTypeMustHaveConstructor
79 .WithArguments( (baseMementoType, baseTypeDefinition) ) );
80
81 builder.SkipAspect();
82
83 return;
84 }
85
86 if ( baseMementoConstructor.Accessibility is not (Metalama.Framework.Code.Accessibility
87 .Protected or Metalama.Framework.Code.Accessibility.Public) )
88 {
89 builder.Diagnostics.Report(
90 DiagnosticDefinitions.MementoConstructorMustBePublicOrProtected
91 .WithArguments( baseMementoConstructor ) );
92
93 builder.SkipAspect();
94
95 return;
96 }
97 }
98 else
99 {
100 baseMementoType = null;
101 baseMementoConstructor = null;
102 }
103
104 //
105
106 //
107 // Introduce a new private nested class called Memento.
108 var mementoType =
109 builder.IntroduceClass(
110 "Memento",
111 whenExists: OverrideStrategy.New,
112 buildType: b =>
113 {
114 b.Accessibility = Metalama.Framework.Code.Accessibility.Protected;
115 b.BaseType = baseMementoType;
116 } );
117
118 //
119
120 //
121 var originatorFieldsAndProperties = builder.Target.FieldsAndProperties
122 .Where( p => p is
123 {
124 IsStatic: false,
125 IsAutoPropertyOrField: true,
126 IsImplicitlyDeclared: false,
127 Writeability: Writeability.All
128 } )
129 .Where( p =>
130 !p.Attributes.OfAttributeType( typeof(MementoIgnoreAttribute) )
131 .Any() );
132
133 //
134
135 //
136 // Introduce data properties to the Memento class for each field of the target class.
137 var propertyMap = new Dictionary<IFieldOrProperty, IProperty>();
138
139 foreach ( var fieldOrProperty in originatorFieldsAndProperties )
140 {
141 var introducedField = mementoType.IntroduceProperty(
142 nameof(this.MementoProperty),
143 buildProperty: b =>
144 {
145 var trimmedName = fieldOrProperty.Name.TrimStart( '_' );
146
147 b.Name = trimmedName.Substring( 0, 1 ).ToUpperInvariant() +
148 trimmedName.Substring( 1 );
149
150 b.Type = fieldOrProperty.Type;
151 } );
152
153 propertyMap.Add( fieldOrProperty, introducedField.Declaration );
154 }
155
156 //
157
158 //
159 // Add a constructor to the Memento class that records the state of the originator.
160 mementoType.IntroduceConstructor(
161 nameof(this.MementoConstructorTemplate),
162 buildConstructor: b =>
163 {
164 var parameter = b.AddParameter( "originator", builder.Target );
165
166 if ( baseMementoConstructor != null )
167 {
168 b.InitializerKind = ConstructorInitializerKind.Base;
169 b.AddInitializerArgument( parameter );
170 }
171 } );
172
173 //
174
175 //
176 // Implement the IMemento interface on the Memento class and add its members.
177 mementoType.ImplementInterface(
178 typeof(IMemento),
179 whenExists: OverrideStrategy.Ignore );
180
181 var introducePropertyResult = mementoType.IntroduceProperty(
182 nameof(this.Originator),
183 whenExists: OverrideStrategy.Ignore );
184
185 var originatorProperty = introducePropertyResult.Outcome == AdviceOutcome.Default
186 ? introducePropertyResult.Declaration
187 : null;
188
189 //
190
191 // Implement the rest of the IOriginator interface and its members.
192 builder.ImplementInterface( typeof(IMementoable), OverrideStrategy.Ignore );
193
194 builder.IntroduceMethod(
195 nameof(this.SaveToMemento),
196 whenExists: OverrideStrategy.Override,
197 buildMethod: m => m.IsVirtual = !builder.Target.IsSealed,
198 args: new { mementoType = mementoType.Declaration } );
199
200 builder.IntroduceMethod(
201 nameof(this.RestoreMemento),
202 buildMethod: m => m.IsVirtual = !builder.Target.IsSealed,
203 whenExists: OverrideStrategy.Override );
204
205 // Pass the state to the templates.
206 //
207 builder.Tags = new BuildAspectInfo(
208 mementoType.Declaration,
209 propertyMap,
210 originatorProperty );
211
212 //
213 }
214
215 [Template]
216 public object? MementoProperty { get; }
217
218 [Template]
219 public IMementoable? Originator { get; }
220
221 [Template]
222 public IMemento SaveToMemento()
223 {
224 //
225 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
226
227 //
228
229 // Invoke the constructor of the Memento class and pass this object as the originator.
230 return buildAspectInfo.MementoType.Constructors.Single()
231 .Invoke( (IExpression) meta.This )!;
232 }
233
234 [Template]
235 public void RestoreMemento( IMemento memento )
236 {
237 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
238
239 // Call the base method if any.
240 meta.Proceed();
241
242 var typedMemento = meta.Cast( buildAspectInfo.MementoType, memento );
243
244 // Set fields of this instance to the values stored in the Memento.
245 foreach ( var pair in buildAspectInfo.PropertyMap )
246 {
247 pair.Key.Value = pair.Value.WithObject( (IExpression) typedMemento ).Value;
248 }
249 }
250
251 //
252 [Template]
253 public void MementoConstructorTemplate()
254 {
255 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
256
257 // Set the originator property and the data properties of the Memento.
258 if ( buildAspectInfo.OriginatorProperty != null )
259 {
260 buildAspectInfo.OriginatorProperty.Value = meta.Target.Parameters[0];
261 }
262 else
263 {
264 // We are in a derived type and there is no need to assign the property.
265 }
266
267 foreach ( var pair in buildAspectInfo.PropertyMap )
268 {
269 pair.Value.Value = pair.Key.WithObject( meta.Target.Parameters[0] ).Value;
270 }
271 }
272
273 //
274}