In this article, we show how to implement the Memento pattern. We will do this in the context of a simple WPF application that tracks fish in a home aquarium. We will intentionally ignore type inheritance and cover this requirement in the second step.
Pattern overview
At the heart of the Memento pattern, we have the following interface:
1public interface IMementoable
2{
3 IMemento SaveToMemento();
4
5 void RestoreMemento( IMemento memento );
6}
Note
This interface is named IOriginator
in the classic Gang-of-Four book. We continue to refer to this object as the originator in the context of this article.
The memento class is typically a private nested class implementing the following interface:
1public interface IMemento
2{
3 IMementoable Originator { get; }
4}
Our objective is to generate the code supporting the SaveToMemento
and RestoreMemento
methods in the following class:
1using Metalama.Patterns.Observability;
2
3[Memento]
4[Observable]
5public partial class Fish
6{
7 public string? Name { get; set; }
8
9 public string? Species { get; set; }
10
11 public DateTime DateAdded { get; set; }
12}
1using System;
2using System.ComponentModel;
3using Metalama.Patterns.Observability;
4
5[Memento]
6[Observable]
7public partial class Fish
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 string? _species;
14
15 public string? Species { get { return this._species; } set { if (!object.ReferenceEquals(value, this._species)) { this._species = value; this.OnPropertyChanged("Species"); } } }
16 private DateTime _dateAdded;
17
18 public DateTime DateAdded { get { return this._dateAdded; } set { if (this._dateAdded != value) { this._dateAdded = value; this.OnPropertyChanged("DateAdded"); } } }
19 protected virtual void OnPropertyChanged(string propertyName)
20 {
21 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
22 }
23 public void RestoreMemento(IMemento memento)
24 {
25 var typedMemento = (Memento)memento;
26 this.Name = ((Memento)typedMemento).Name;
27 this.Species = ((Memento)typedMemento).Species;
28 this.DateAdded = ((Memento)typedMemento).DateAdded;
29 }
30 public IMemento SaveToMemento()
31 {
32 return new Memento(this);
33 }
34 public event PropertyChangedEventHandler? PropertyChanged;
35
36 private class Memento : IMemento
37 {
38 public Memento(Fish originator)
39 {
40 this.Originator = originator;
41 this.Name = originator.Name;
42 this.Species = originator.Species;
43 this.DateAdded = originator.DateAdded;
44 }
45 public DateTime DateAdded { get; }
46 public string? Name { get; }
47 public IMementoable? Originator { get; }
48 public string? Species { get; }
49 }
50}
Note
This example also uses the [Observable] aspect to implement the INotifyPropertyChanged
interface.
How can we implement this aspect?
Strategizing
The first step is to list all the code operations that we need to perform:
- Add a nested type named
Memento
with the following members:- The
IMemento
interface and itsOriginator
property. - A private field for each field or automatic property of the originator (
IMementoable
) object, copying its name and property. - A constructor that accepts the originator types as an argument and copies its fields and properties to the private fields of the
Memento
object.
- The
- Implement the
IMementoable
interface with the following members:- The
SaveToMemento
method that returns an instance of the newMemento
type (effectively returning a copy of the state of the object). - The
RestoreMemento
method that copies the properties of theMemento
object back to the fields and properties of the originator.
- The
Passing state between BuildAspect and the templates
As always with non-trivial Metalama aspects, our BuildAspect
method performs the code analysis and adds or overrides members using the IAspectBuilder
advising API. Templates implementing the new methods and constructors read this state.
The following state encapsulates the state that is shared between BuildAspect
and the templates:
7[CompileTime]
8private record BuildAspectInfo(
9
10 // The newly introduced Memento type.
11 INamedType MementoType,
12
13 // Mapping from fields or properties in the Originator to the corresponding property
14 // in the Memento type.
15 Dictionary<IFieldOrProperty, IProperty> PropertyMap,
16
17 // The Originator property in the new Memento type.
18 IProperty OriginatorProperty );
19
The crucial part is the PropertyMap
dictionary, which maps fields and properties of the originator class to the corresponding property of the Memento type.
We use the Tags
facility to pass state between BuildAspect
and the templates. At the end of BuildAspect
, we set the tag:
109builder.Tags = new BuildAspectInfo(
110 mementoType.Declaration,
111 propertyMap,
112 originatorProperty.Declaration );
113
Then, in the templates, we read it:
127var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
128
For details regarding state sharing, see Sharing state with advice.
Step 1. Introducing the Memento type
The first step is to introduce a nested type named Memento
.
23// Introduce a new private nested class called Memento.
24var mementoType =
25 builder.IntroduceClass(
26 "Memento",
27 buildType: b =>
28 b.Accessibility =
29 Metalama.Framework.Code.Accessibility.Private );
30
We store the result in a local variable named mementoType
. We will use it to construct the type.
For details regarding type introduction, see Introducing types.
Step 2. Introducing and mapping the properties
We select the mutable fields and automatic properties, except those that have a [MementoIgnore]
attribute.
34var originatorFieldsAndProperties = builder.Target.FieldsAndProperties
35 .Where(
36 p => p is
37 {
38 IsStatic: false,
39 IsAutoPropertyOrField: true,
40 IsImplicitlyDeclared: false,
41 Writeability: Writeability.All
42 } )
43 .Where(
44 p =>
45 !p.Attributes.OfAttributeType( typeof(MementoIgnoreAttribute) )
46 .Any() );
47
We iterate through this list and create the corresponding public property in the new Memento
type. While doing this, we update the propertyMap
dictionary, mapping the originator type field or property to the Memento
type property.
51// Introduce data properties to the Memento class for each field of the target class.
52var propertyMap = new Dictionary<IFieldOrProperty, IProperty>();
53
54foreach ( var fieldOrProperty in originatorFieldsAndProperties )
55{
56 var introducedField = mementoType.IntroduceProperty(
57 nameof(this.MementoProperty),
58 buildProperty: b =>
59 {
60 var trimmedName = fieldOrProperty.Name.TrimStart( '_' );
61
62 b.Name = trimmedName.Substring( 0, 1 ).ToUpperInvariant() +
63 trimmedName.Substring( 1 );
64
65 b.Type = fieldOrProperty.Type;
66 } );
67
68 propertyMap.Add( fieldOrProperty, introducedField.Declaration );
69}
70
Here is the template for these properties:
117[Template]
118public object? MementoProperty { get; }
119
Step 3. Adding the Memento constructor
Now that we have the properties and the mapping, we can generate the constructor of the Memento
type.
74// Add a constructor to the Memento class that records the state of the originator.
75mementoType.IntroduceConstructor(
76 nameof(this.MementoConstructorTemplate),
77 buildConstructor: b => { b.AddParameter( "originator", builder.Target ); } );
78
Here is the constructor template. We iterate the PropertyMap
to set the Memento
properties from the originator.
151[Template]
152public void MementoConstructorTemplate()
153{
154 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
155
156 // Set the originator property and the data properties of the Memento.
157 buildAspectInfo.OriginatorProperty.Value = meta.Target.Parameters[0].Value;
158
159 foreach ( var pair in buildAspectInfo.PropertyMap )
160 {
161 pair.Value.Value = pair.Key.With( meta.Target.Parameters[0] ).Value;
162 }
163}
164
Step 4. Implementing the IMemento interface in the Memento type
Let's now implement the IMemento
interface in the Memento
nested type. Here is the BuildAspect
code:
82// Implement the IMemento interface on the Memento class and add its members.
83mementoType.ImplementInterface(
84 typeof(IMemento),
85 whenExists: OverrideStrategy.Ignore );
86
87var originatorProperty =
88 mementoType.IntroduceProperty( nameof(this.Originator) );
89
This interface has a single member:
120[Template]
121public IMementoable? Originator { get; }
122
Step 5. Implementing the IMementoable interface in the originator type
We can finally implement the IMementoable
interface.
93// Implement the rest of the IOriginator interface and its members.
94builder.ImplementInterface( typeof(IMementoable) );
95
96builder.IntroduceMethod(
97 nameof(this.SaveToMemento),
98 whenExists: OverrideStrategy.Override,
99 args: new { mementoType = mementoType.Declaration } );
100
101builder.IntroduceMethod(
102 nameof(this.RestoreMemento),
103 whenExists: OverrideStrategy.Override );
104
Here are the interface members:
123[Template]
124public IMemento SaveToMemento()
125{
127 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
128
130
131 // Invoke the constructor of the Memento class and pass this object as the originator.
132 return buildAspectInfo.MementoType.Constructors.Single()
133 .Invoke( (IExpression) meta.This )!;
134}
135
136[Template]
137public void RestoreMemento( IMemento memento )
138{
139 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
140
141 var typedMemento = meta.Cast( buildAspectInfo.MementoType, memento );
142
143 // Set fields of this instance to the values stored in the Memento.
144 foreach ( var pair in buildAspectInfo.PropertyMap )
145 {
146 pair.Key.Value = pair.Value.With( (IExpression) typedMemento ).Value;
147 }
148}
149
Note again the use of the property mapping in the RestoreMemento
method.
Complete aspect
Let's now put all the bits together. Here is the complete source code of our aspect, MementoAttribute
.
1using Metalama.Framework.Advising;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4
5public sealed class MementoAttribute : TypeAspect
6{
7 [CompileTime]
8 private record BuildAspectInfo(
9
10 // The newly introduced Memento type.
11 INamedType MementoType,
12
13 // Mapping from fields or properties in the Originator to the corresponding property
14 // in the Memento type.
15 Dictionary<IFieldOrProperty, IProperty> PropertyMap,
16
17 // The Originator property in the new Memento type.
18 IProperty OriginatorProperty );
19
20 public override void BuildAspect( IAspectBuilder<INamedType> builder )
21 {
22 //
23 // Introduce a new private nested class called Memento.
24 var mementoType =
25 builder.IntroduceClass(
26 "Memento",
27 buildType: b =>
28 b.Accessibility =
29 Metalama.Framework.Code.Accessibility.Private );
30
31 //
32
33 //
34 var originatorFieldsAndProperties = builder.Target.FieldsAndProperties
35 .Where(
36 p => p is
37 {
38 IsStatic: false,
39 IsAutoPropertyOrField: true,
40 IsImplicitlyDeclared: false,
41 Writeability: Writeability.All
42 } )
43 .Where(
44 p =>
45 !p.Attributes.OfAttributeType( typeof(MementoIgnoreAttribute) )
46 .Any() );
47
48 //
49
50 //
51 // Introduce data properties to the Memento class for each field of the target class.
52 var propertyMap = new Dictionary<IFieldOrProperty, IProperty>();
53
54 foreach ( var fieldOrProperty in originatorFieldsAndProperties )
55 {
56 var introducedField = mementoType.IntroduceProperty(
57 nameof(this.MementoProperty),
58 buildProperty: b =>
59 {
60 var trimmedName = fieldOrProperty.Name.TrimStart( '_' );
61
62 b.Name = trimmedName.Substring( 0, 1 ).ToUpperInvariant() +
63 trimmedName.Substring( 1 );
64
65 b.Type = fieldOrProperty.Type;
66 } );
67
68 propertyMap.Add( fieldOrProperty, introducedField.Declaration );
69 }
70
71 //
72
73 //
74 // Add a constructor to the Memento class that records the state of the originator.
75 mementoType.IntroduceConstructor(
76 nameof(this.MementoConstructorTemplate),
77 buildConstructor: b => { b.AddParameter( "originator", builder.Target ); } );
78
79 //
80
81 //
82 // Implement the IMemento interface on the Memento class and add its members.
83 mementoType.ImplementInterface(
84 typeof(IMemento),
85 whenExists: OverrideStrategy.Ignore );
86
87 var originatorProperty =
88 mementoType.IntroduceProperty( nameof(this.Originator) );
89
90 //
91
92 //
93 // Implement the rest of the IOriginator interface and its members.
94 builder.ImplementInterface( typeof(IMementoable) );
95
96 builder.IntroduceMethod(
97 nameof(this.SaveToMemento),
98 whenExists: OverrideStrategy.Override,
99 args: new { mementoType = mementoType.Declaration } );
100
101 builder.IntroduceMethod(
102 nameof(this.RestoreMemento),
103 whenExists: OverrideStrategy.Override );
104
105 //
106
107 // Pass the state to the templates.
108 //
109 builder.Tags = new BuildAspectInfo(
110 mementoType.Declaration,
111 propertyMap,
112 originatorProperty.Declaration );
113
114 //
115 }
116
117 [Template]
118 public object? MementoProperty { get; }
119
120 [Template]
121 public IMementoable? Originator { get; }
122
123 [Template]
124 public IMemento SaveToMemento()
125 {
126 //
127 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
128
129 //
130
131 // Invoke the constructor of the Memento class and pass this object as the originator.
132 return buildAspectInfo.MementoType.Constructors.Single()
133 .Invoke( (IExpression) meta.This )!;
134 }
135
136 [Template]
137 public void RestoreMemento( IMemento memento )
138 {
139 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
140
141 var typedMemento = meta.Cast( buildAspectInfo.MementoType, memento );
142
143 // Set fields of this instance to the values stored in the Memento.
144 foreach ( var pair in buildAspectInfo.PropertyMap )
145 {
146 pair.Key.Value = pair.Value.With( (IExpression) typedMemento ).Value;
147 }
148 }
149
150 //
151 [Template]
152 public void MementoConstructorTemplate()
153 {
154 var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
155
156 // Set the originator property and the data properties of the Memento.
157 buildAspectInfo.OriginatorProperty.Value = meta.Target.Parameters[0].Value;
158
159 foreach ( var pair in buildAspectInfo.PropertyMap )
160 {
161 pair.Value.Value = pair.Key.With( meta.Target.Parameters[0] ).Value;
162 }
163 }
164
165 //
166}
This implementation does not support type inheritance, i.e., a memento-able object cannot inherit from another memento-able object. In the next article, we will see how to support type inheritance.