Open sandboxFocusImprove this doc

Memento example, step 1: a basic aspect

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:

Source Code


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}
Transformed Code
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:

  1. Add a nested type named Memento with the following members:
    • The IMemento interface and its Originator 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.
  2. Implement the IMementoable interface with the following members:
    • The SaveToMemento method that returns an instance of the new Memento type (effectively returning a copy of the state of the object).
    • The RestoreMemento method that copies the properties of the Memento object back to the fields and properties of the originator.

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:

6[CompileTime]
7private record BuildAspectInfo(
8
9    // The newly introduced Memento type.
10    INamedType MementoType,
11
12    // Mapping from fields or properties in the Originator to the corresponding property
13    // in the Memento type.
14    Dictionary<IFieldOrProperty, IProperty> PropertyMap,
15
16    // The Originator property in the new Memento type.
17    IProperty OriginatorProperty );
18

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:

106builder.Tags = new BuildAspectInfo(
107    mementoType.Declaration,
108    propertyMap,
109    originatorProperty.Declaration );
110

Then, in the templates, we read it:

124var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
125

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.

22// Introduce a new private nested class called Memento.
23var mementoType =
24    builder.IntroduceClass(
25        "Memento",
26        buildType: b =>
27            b.Accessibility =
28                Metalama.Framework.Code.Accessibility.Private );
29

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.

33var originatorFieldsAndProperties = builder.Target.FieldsAndProperties
34    .Where( p => p is
35    {
36        IsStatic: false,
37        IsAutoPropertyOrField: true,
38        IsImplicitlyDeclared: false,
39        Writeability: Writeability.All
40    } )
41    .Where( p =>
42                !p.Attributes.OfAttributeType( typeof(MementoIgnoreAttribute) )
43                    .Any() );
44

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.

48// Introduce data properties to the Memento class for each field of the target class.
49var propertyMap = new Dictionary<IFieldOrProperty, IProperty>();
50
51foreach ( var fieldOrProperty in originatorFieldsAndProperties )
52{
53    var introducedField = mementoType.IntroduceProperty(
54        nameof(this.MementoProperty),
55        buildProperty: b =>
56        {
57            var trimmedName = fieldOrProperty.Name.TrimStart( '_' );
58
59            b.Name = trimmedName.Substring( 0, 1 ).ToUpperInvariant() +
60                     trimmedName.Substring( 1 );
61
62            b.Type = fieldOrProperty.Type;
63        } );
64
65    propertyMap.Add( fieldOrProperty, introducedField.Declaration );
66}
67

Here is the template for these properties:

114[Template]
115public object? MementoProperty { get; }
116

Step 3. Adding the Memento constructor

Now that we have the properties and the mapping, we can generate the constructor of the Memento type.

71// Add a constructor to the Memento class that records the state of the originator.
72mementoType.IntroduceConstructor(
73    nameof(this.MementoConstructorTemplate),
74    buildConstructor: b => { b.AddParameter( "originator", builder.Target ); } );
75

Here is the constructor template. We iterate the PropertyMap to set the Memento properties from the originator.

148[Template]
149public void MementoConstructorTemplate()
150{
151    var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
152
153    // Set the originator property and the data properties of the Memento.
154    buildAspectInfo.OriginatorProperty.Value = meta.Target.Parameters[0].Value;
155
156    foreach ( var pair in buildAspectInfo.PropertyMap )
157    {
158        pair.Value.Value = pair.Key.WithObject( meta.Target.Parameters[0] ).Value;
159    }
160}
161

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:

79// Implement the IMemento interface on the Memento class and add its members.   
80mementoType.ImplementInterface(
81    typeof(IMemento),
82    whenExists: OverrideStrategy.Ignore );
83
84var originatorProperty =
85    mementoType.IntroduceProperty( nameof(this.Originator) );
86

This interface has a single member:

117[Template]
118public IMementoable? Originator { get; }
119

Step 5. Implementing the IMementoable interface in the originator type

We can finally implement the IMementoable interface.

90// Implement the rest of the IOriginator interface and its members.
91builder.ImplementInterface( typeof(IMementoable) );
92
93builder.IntroduceMethod(
94    nameof(this.SaveToMemento),
95    whenExists: OverrideStrategy.Override,
96    args: new { mementoType = mementoType.Declaration } );
97
98builder.IntroduceMethod(
99    nameof(this.RestoreMemento),
100    whenExists: OverrideStrategy.Override );
101

Here are the interface members:

120[Template]
121public IMemento SaveToMemento()
122{
124    var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
125
127
128    // Invoke the constructor of the Memento class and pass this object as the originator.
129    return buildAspectInfo.MementoType.Constructors.Single()
130        .Invoke( (IExpression) meta.This )!;
131}
132

133[Template]
134public void RestoreMemento( IMemento memento )
135{
136    var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
137
138    var typedMemento = meta.Cast( buildAspectInfo.MementoType, memento );
139
140    // Set fields of this instance to the values stored in the Memento.
141    foreach ( var pair in buildAspectInfo.PropertyMap )
142    {
143        pair.Key.Value = pair.Value.WithObject( (IExpression) typedMemento ).Value;
144    }
145}
146

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.Aspects;
2using Metalama.Framework.Code;
3
4public sealed class MementoAttribute : TypeAspect
5{
6    [CompileTime]
7    private record BuildAspectInfo(
8
9        // The newly introduced Memento type.
10        INamedType MementoType,
11
12        // Mapping from fields or properties in the Originator to the corresponding property
13        // in the Memento type.
14        Dictionary<IFieldOrProperty, IProperty> PropertyMap,
15
16        // The Originator property in the new Memento type.
17        IProperty OriginatorProperty );
18
19    public override void BuildAspect( IAspectBuilder<INamedType> builder )
20    {
21        // 
22        // Introduce a new private nested class called Memento.
23        var mementoType =
24            builder.IntroduceClass(
25                "Memento",
26                buildType: b =>
27                    b.Accessibility =
28                        Metalama.Framework.Code.Accessibility.Private );
29
30        // 
31
32        // 
33        var originatorFieldsAndProperties = builder.Target.FieldsAndProperties
34            .Where( p => p is
35            {
36                IsStatic: false,
37                IsAutoPropertyOrField: true,
38                IsImplicitlyDeclared: false,
39                Writeability: Writeability.All
40            } )
41            .Where( p =>
42                        !p.Attributes.OfAttributeType( typeof(MementoIgnoreAttribute) )
43                            .Any() );
44
45        // 
46
47        // 
48        // Introduce data properties to the Memento class for each field of the target class.
49        var propertyMap = new Dictionary<IFieldOrProperty, IProperty>();
50
51        foreach ( var fieldOrProperty in originatorFieldsAndProperties )
52        {
53            var introducedField = mementoType.IntroduceProperty(
54                nameof(this.MementoProperty),
55                buildProperty: b =>
56                {
57                    var trimmedName = fieldOrProperty.Name.TrimStart( '_' );
58
59                    b.Name = trimmedName.Substring( 0, 1 ).ToUpperInvariant() +
60                             trimmedName.Substring( 1 );
61
62                    b.Type = fieldOrProperty.Type;
63                } );
64
65            propertyMap.Add( fieldOrProperty, introducedField.Declaration );
66        }
67
68        // 
69
70        // 
71        // Add a constructor to the Memento class that records the state of the originator.
72        mementoType.IntroduceConstructor(
73            nameof(this.MementoConstructorTemplate),
74            buildConstructor: b => { b.AddParameter( "originator", builder.Target ); } );
75
76        // 
77
78        // 
79        // Implement the IMemento interface on the Memento class and add its members.   
80        mementoType.ImplementInterface(
81            typeof(IMemento),
82            whenExists: OverrideStrategy.Ignore );
83
84        var originatorProperty =
85            mementoType.IntroduceProperty( nameof(this.Originator) );
86
87        // 
88
89        // 
90        // Implement the rest of the IOriginator interface and its members.
91        builder.ImplementInterface( typeof(IMementoable) );
92
93        builder.IntroduceMethod(
94            nameof(this.SaveToMemento),
95            whenExists: OverrideStrategy.Override,
96            args: new { mementoType = mementoType.Declaration } );
97
98        builder.IntroduceMethod(
99            nameof(this.RestoreMemento),
100            whenExists: OverrideStrategy.Override );
101
102        // 
103
104        // Pass the state to the templates.
105        // 
106        builder.Tags = new BuildAspectInfo(
107            mementoType.Declaration,
108            propertyMap,
109            originatorProperty.Declaration );
110
111        // 
112    }
113
114    [Template]
115    public object? MementoProperty { get; }
116
117    [Template]
118    public IMementoable? Originator { get; }
119
120    [Template]
121    public IMemento SaveToMemento()
122    {
123        // 
124        var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
125
126        // 
127
128        // Invoke the constructor of the Memento class and pass this object as the originator.
129        return buildAspectInfo.MementoType.Constructors.Single()
130            .Invoke( (IExpression) meta.This )!;
131    }
132
133    [Template]
134    public void RestoreMemento( IMemento memento )
135    {
136        var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
137
138        var typedMemento = meta.Cast( buildAspectInfo.MementoType, memento );
139
140        // Set fields of this instance to the values stored in the Memento.
141        foreach ( var pair in buildAspectInfo.PropertyMap )
142        {
143            pair.Key.Value = pair.Value.WithObject( (IExpression) typedMemento ).Value;
144        }
145    }
146
147    // 
148    [Template]
149    public void MementoConstructorTemplate()
150    {
151        var buildAspectInfo = (BuildAspectInfo) meta.Tags.Source!;
152
153        // Set the originator property and the data properties of the Memento.
154        buildAspectInfo.OriginatorProperty.Value = meta.Target.Parameters[0].Value;
155
156        foreach ( var pair in buildAspectInfo.PropertyMap )
157        {
158            pair.Value.Value = pair.Key.WithObject( meta.Target.Parameters[0] ).Value;
159        }
160    }
161
162    // 
163}

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.