Memento Generator
Overview
The Memento Generator creates immutable snapshot structs and optional caretaker (history) classes for implementing undo/redo functionality. It eliminates boilerplate by generating Capture and Restore methods, version tracking, and history management.
When to Use
Use the Memento generator when you need to:
- Implement undo/redo: Track state changes and restore previous states
- Create snapshots: Capture object state at specific points in time
- Support time travel debugging: Navigate through state history
- Persist state safely: Immutable mementos are safe to store and share
Installation
The generator is included in the PatternKit.Generators package:
dotnet add package PatternKit.Generators
Quick Start
using PatternKit.Generators;
[Memento(GenerateCaretaker = true)]
public partial class Document
{
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public int CursorPosition { get; set; }
}
Generated:
// Immutable snapshot struct
public readonly partial struct DocumentMemento
{
public int MementoVersion => /* hash */;
public string Title { get; }
public string Content { get; }
public int CursorPosition { get; }
public static DocumentMemento Capture(in Document originator);
public Document RestoreNew();
public void Restore(Document originator);
}
// History manager (when GenerateCaretaker = true)
public sealed partial class DocumentHistory
{
public int Count { get; }
public bool CanUndo { get; }
public bool CanRedo { get; }
public Document Current { get; }
public void Capture(Document state);
public bool Undo();
public bool Redo();
public void Clear(Document initial);
}
Usage:
// Manual snapshot
var doc = new Document { Title = "Hello", Content = "World", CursorPosition = 5 };
var memento = DocumentMemento.Capture(in doc);
doc.Title = "Changed";
var restored = memento.RestoreNew(); // Back to "Hello"
// With history (undo/redo)
var history = new DocumentHistory(new Document());
history.Capture(new Document { Title = "v1" });
history.Capture(new Document { Title = "v2" });
history.Capture(new Document { Title = "v3" });
history.Undo(); // Current.Title == "v2"
history.Undo(); // Current.Title == "v1"
history.Redo(); // Current.Title == "v2"
Records Support
The generator works seamlessly with records, using positional constructors when available:
[Memento(GenerateCaretaker = true, Capacity = 100, SkipDuplicates = true)]
public partial record class EditorState(string Text, int Cursor, int SelectionLength)
{
public bool HasSelection => SelectionLength > 0;
public EditorState Insert(string text)
{
// Immutable update
return this with { Text = Text.Insert(Cursor, text), Cursor = Cursor + text.Length };
}
}
The memento RestoreNew() method will use the record's primary constructor.
Attributes
[Memento]
Main attribute for marking types to generate memento support.
| Property | Type | Default | Description |
|---|---|---|---|
GenerateCaretaker |
bool |
false |
Generate history class for undo/redo |
Capacity |
int |
0 |
Max history entries (0 = unlimited) |
InclusionMode |
MementoInclusionMode |
IncludeAll |
How to select members |
SkipDuplicates |
bool |
true |
Skip capture if state equals current |
[MementoIgnore]
Excludes a member from memento capture (when using IncludeAll mode):
[Memento]
public partial class EditorState
{
public string Content { get; set; } = "";
[MementoIgnore]
public bool IsDirty { get; set; } // Not captured
}
[MementoInclude]
Explicitly includes a member (when using OptIn mode):
[Memento(InclusionMode = MementoInclusionMode.OptIn)]
public partial class EditorState
{
[MementoInclude]
public string Content { get; set; } = ""; // Captured
public DateTime LastModified { get; set; } // Not captured
}
[MementoStrategy]
Controls how reference types are captured:
[Memento]
public partial class GameState
{
public int Score { get; set; }
[MementoStrategy(CaptureStrategy.Clone)]
public List<string> Items { get; set; } = new(); // Cloned on capture
}
| Strategy | Description |
|---|---|
ByReference |
Store reference directly (default for value types, strings) |
Clone |
Call ICloneable.Clone() or use custom cloner |
DeepCopy |
Serialize/deserialize for deep copy |
Caretaker (History) Features
When GenerateCaretaker = true, a history class is generated:
Properties
| Property | Type | Description |
|---|---|---|
Count |
int |
Total states in history |
CanUndo |
bool |
True if undo is possible |
CanRedo |
bool |
True if redo is possible |
Current |
T |
Current state (or default if empty) |
Methods
| Method | Description |
|---|---|
Capture(T state) |
Adds state to history, truncating forward history |
Undo() |
Moves to previous state; returns false if at start |
Redo() |
Moves to next state; returns false if at end |
Clear(T initial) |
Clears history and sets initial state |
History Behavior
[A] → [B] → [C] → [D] // Capture A, B, C, D
↑
Current (D)
Undo():
[A] → [B] → [C] → [D]
↑
Current (C)
Undo():
[A] → [B] → [C] → [D]
↑
Current (B)
Capture(E): // Truncates forward history!
[A] → [B] → [E]
↑
Current (E)
Diagnostics
| ID | Severity | Description |
|---|---|---|
| PKMEM001 | Error | Type must be declared as partial |
| PKMEM002 | Warning | Member is inaccessible for capture/restore |
| PKMEM003 | Warning | Mutable reference type captured by reference (mutations affect all snapshots) |
| PKMEM004 | Error | Clone strategy requested but no clone mechanism available |
| PKMEM005 | Error | Cannot generate RestoreNew for record (no accessible constructor) |
| PKMEM006 | Info | Init-only/readonly members prevent in-place restore |
Best Practices
1. Use Immutable Types When Possible
Records with with expressions make state management cleaner:
[Memento(GenerateCaretaker = true)]
public partial record class AppState(int Counter, string Status)
{
public AppState Increment() => this with { Counter = Counter + 1 };
}
2. Set Capacity for Long-Running Applications
Prevent memory issues by limiting history size:
[Memento(GenerateCaretaker = true, Capacity = 1000)]
public partial class DocumentState { }
3. Skip Duplicates for Frequent Updates
Avoid cluttering history with identical states:
[Memento(GenerateCaretaker = true, SkipDuplicates = true)]
public partial record struct Position(int X, int Y);
4. Handle Reference Types Carefully
Mutable reference types can cause issues if modified after capture:
[Memento]
public partial class GameState
{
// ⚠️ Warning: List mutations affect all mementos
public List<Item> Inventory { get; set; } = new();
// ✅ Better: Use immutable collection or Clone strategy
[MementoStrategy(CaptureStrategy.Clone)]
public List<Item> Inventory { get; set; } = new();
}
Examples
Text Editor with Undo
[Memento(GenerateCaretaker = true, Capacity = 100, SkipDuplicates = true)]
public partial record class EditorState(string Text, int Cursor, int SelectionLength)
{
public static EditorState Empty() => new("", 0, 0);
public EditorState Insert(string text) =>
this with { Text = Text.Insert(Cursor, text), Cursor = Cursor + text.Length };
public EditorState Backspace() =>
Cursor == 0 ? this : this with { Text = Text.Remove(Cursor - 1, 1), Cursor = Cursor - 1 };
}
public sealed class TextEditor
{
private readonly EditorStateHistory _history;
public TextEditor() => _history = new(EditorState.Empty());
public EditorState Current => _history.Current;
public bool CanUndo => _history.CanUndo;
public bool CanRedo => _history.CanRedo;
public void Apply(Func<EditorState, EditorState> operation)
{
_history.Capture(operation(Current));
}
public bool Undo() => _history.Undo();
public bool Redo() => _history.Redo();
}
// Usage
var editor = new TextEditor();
editor.Apply(s => s.Insert("Hello"));
editor.Apply(s => s.Insert(" World"));
Console.WriteLine(editor.Current.Text); // "Hello World"
editor.Undo();
Console.WriteLine(editor.Current.Text); // "Hello"
Game State Snapshots
[Memento]
public partial class GameState
{
public int Score { get; set; }
public int Level { get; set; }
public int Lives { get; set; } = 3;
public string PlayerName { get; set; } = "";
}
// Quick save/load
var game = new GameState { Score = 1000, Level = 5, PlayerName = "Player1" };
var quickSave = GameStateMemento.Capture(in game);
// Player dies...
game.Lives--;
game.Score -= 100;
// Quick load
var restored = quickSave.RestoreNew();
Console.WriteLine($"Restored: Score={restored.Score}, Lives={restored.Lives}");
Application State with Limited History
[Memento(GenerateCaretaker = true, Capacity = 50)]
public partial record class AppSettings(
string Theme,
int FontSize,
bool DarkMode)
{
public static AppSettings Default => new("Default", 14, false);
}
public class SettingsManager
{
private readonly AppSettingsHistory _history;
public SettingsManager() => _history = new(AppSettings.Default);
public AppSettings Current => _history.Current;
public void UpdateTheme(string theme)
=> _history.Capture(Current with { Theme = theme });
public void UpdateFontSize(int size)
=> _history.Capture(Current with { FontSize = size });
public void Reset()
=> _history.Clear(AppSettings.Default);
}
Troubleshooting
PKMEM001: Type must be partial
Cause: Target type is not marked partial.
Fix:
// ❌ Wrong
[Memento]
public class State { }
// ✅ Correct
[Memento]
public partial class State { }
PKMEM003: Mutable reference capture warning
Cause: A mutable reference type (List, Dictionary, etc.) is captured by reference.
Fix: Use [MementoStrategy(Clone)] or immutable types:
// Option 1: Clone strategy
[MementoStrategy(CaptureStrategy.Clone)]
public List<Item> Items { get; set; }
// Option 2: Use immutable collection
public ImmutableList<Item> Items { get; set; } = ImmutableList<Item>.Empty;
PKMEM006: Init-only restriction
Cause: Init-only properties prevent in-place restore.
Note: This is informational. Use RestoreNew() instead of Restore():
var memento = StateMemento.Capture(in state);
var restored = memento.RestoreNew(); // Creates new instance
// memento.Restore(state); // Not available for init-only types
See Also
- Patterns: Memento
- Command Pattern — Often used with Memento for undo