BehavioralC#verifiedVerified
Memento Pattern in C#
Captures and externalizes an object's internal state without violating encapsulation, allowing the object to be restored to that state later.
How to Implement the Memento Pattern in C#
1Step 1: Memento stores a snapshot of the originator's state
public record Memento(string State, DateTime CreatedAt);2Step 2: Originator creates and restores from mementos
public class TextEditor
{
public string Content { get; set; } = "";
public Memento Save() =>
new(Content, DateTime.UtcNow);
public void Restore(Memento memento) =>
Content = memento.State;
}3Step 3: Caretaker manages memento history
public class History
{
private readonly Stack<Memento> _snapshots = new();
public void Push(Memento memento) => _snapshots.Push(memento);
public Memento? Pop() =>
_snapshots.TryPop(out var m) ? m : null;
public int Count => _snapshots.Count;
}
// Usage:
// var editor = new TextEditor { Content = "Hello" };
// var history = new History();
// history.Push(editor.Save());
// editor.Content = "World";
// editor.Restore(history.Pop()!);
// Console.WriteLine(editor.Content); // "Hello"using System.Text.Json;
using Microsoft.Extensions.Logging;
// [step] Define a typed, immutable memento with metadata
public sealed record Memento<T>(
T State, DateTime CreatedAt, string Description,
int Version);
// [step] Generic originator with change tracking
public interface IOriginator<T>
{
T GetState();
void SetState(T state);
}
// [step] Production caretaker with bounded history and serialization
public sealed class MementoCaretaker<T>(
IOriginator<T> originator,
ILogger<MementoCaretaker<T>> logger,
int maxSnapshots = 100)
{
private readonly List<Memento<T>> _history = [];
private int _currentIndex = -1;
public int Count => _history.Count;
public int CurrentIndex => _currentIndex;
public bool CanUndo => _currentIndex > 0;
public bool CanRedo => _currentIndex < _history.Count - 1;
// [step] Save with description and bounded capacity
public Memento<T> Save(string description = "")
{
// Discard any "future" states if we're mid-history
if (_currentIndex < _history.Count - 1)
_history.RemoveRange(
_currentIndex + 1, _history.Count - _currentIndex - 1);
var memento = new Memento<T>(
originator.GetState(), DateTime.UtcNow,
description, _history.Count);
_history.Add(memento);
// Enforce capacity
if (_history.Count > maxSnapshots)
{
_history.RemoveAt(0);
logger.LogDebug("Removed oldest snapshot (cap: {Max})",
maxSnapshots);
}
_currentIndex = _history.Count - 1;
logger.LogDebug("Saved snapshot #{Index}: {Desc}",
_currentIndex, description);
return memento;
}
// [step] Undo and redo navigation
public bool Undo()
{
if (!CanUndo) return false;
_currentIndex--;
originator.SetState(_history[_currentIndex].State);
logger.LogDebug("Undo to #{Index}", _currentIndex);
return true;
}
public bool Redo()
{
if (!CanRedo) return false;
_currentIndex++;
originator.SetState(_history[_currentIndex].State);
logger.LogDebug("Redo to #{Index}", _currentIndex);
return true;
}
// [step] Serialize history for persistence
public string ExportHistory() =>
JsonSerializer.Serialize(_history, new JsonSerializerOptions
{
WriteIndented = true
});
public void ImportHistory(string json)
{
var imported = JsonSerializer.Deserialize<List<Memento<T>>>(json);
if (imported is null) return;
_history.Clear();
_history.AddRange(imported);
_currentIndex = _history.Count - 1;
if (_currentIndex >= 0)
originator.SetState(_history[_currentIndex].State);
}
public IReadOnlyList<Memento<T>> GetHistory() =>
_history.AsReadOnly();
}Memento Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Memento Pattern in the Real World
“Think of a video game save point. When you save, the game (originator) packages your character's stats, inventory, and position into a save file (memento). The save system (caretaker) stores these files without understanding their contents. When you die and reload, the save file is handed back to the game, which restores everything exactly as it was.”