Memento Pattern Real-World Example in C#: Complete Implementation
Most memento pattern tutorials save a string, restore a string, and call it a day. That won't help when your real editor state includes text content, cursor position, and formatting metadata -- all of which need to roll back atomically. This article walks through a complete memento pattern real-world example in C# by building a document editor with full undo/redo support and bounded history management.
You'll get compilable classes for every component: the EditorMemento snapshot, the DocumentEditor originator, and the EditorHistory caretaker. We'll add unit tests that verify undo/redo correctness, enforce history limits to control memory, and discuss what changes when you move this memento pattern implementation into production. If you've worked with the command pattern for operation-based undo, the memento pattern offers a fundamentally different tradeoff -- snapshots instead of deltas.
The Problem: Editor State Without Memento
Consider a document editor that tracks text content, a cursor position, and whether bold formatting is active. Without the memento pattern, undo means manually reversing each field:
public class NaiveEditor
{
public string Content { get; set; } = string.Empty;
public int CursorPosition { get; set; }
public bool IsBold { get; set; }
private readonly Stack<(string, int, bool)> _snapshots = new();
public void Save()
{
_snapshots.Push((Content, CursorPosition, IsBold));
}
public void Undo()
{
if (_snapshots.Count == 0)
{
return;
}
var (content, cursor, bold) = _snapshots.Pop();
Content = content;
CursorPosition = cursor;
IsBold = bold;
}
}
This works for three fields, but the problems compound quickly. The editor class manages its own history -- violating single responsibility. Adding redo means duplicating the stack logic. Adding new state fields means updating the tuple signature everywhere. Testing undo requires going through the editor's public API, and there's no way to limit history depth.
The memento pattern solves this by extracting state snapshots into their own objects. The editor creates mementos without exposing its internals. A separate caretaker manages the history. Let's build it.
Designing the EditorMemento
The memento is the snapshot object. It captures the editor's complete state at a point in time. The critical design constraint is encapsulation -- the memento stores state, but only the originator that created it should be able to read it back.
In C#, we can enforce this with a nested class or by keeping the memento's properties internal. Here's a clean approach using a sealed record:
public sealed record EditorMemento
{
internal string Content { get; }
internal int CursorPosition { get; }
internal bool IsBold { get; }
internal DateTime CreatedAt { get; }
internal EditorMemento(
string content,
int cursorPosition,
bool isBold)
{
Content = content;
CursorPosition = cursorPosition;
IsBold = isBold;
CreatedAt = DateTime.UtcNow;
}
}
Several things to notice here. The internal access modifier prevents external assemblies from reading memento state directly -- only classes within the same assembly can access these properties. The CreatedAt timestamp provides metadata for debugging and audit trails without affecting restoration logic. Using a record gives us value-based equality and immutability for free.
This memento pattern implementation keeps the snapshot clean. The memento doesn't know about the editor, the history, or undo/redo. It's a pure data carrier. If you later add new state fields to the editor -- font size, selection range, scroll position -- you update only the EditorMemento and the originator's CreateMemento/RestoreMemento methods. The caretaker never changes.
Building the DocumentEditor as the Originator
The originator is the object whose state we're capturing. In our memento pattern example, that's the DocumentEditor. It has two responsibilities: creating mementos from its current state and restoring its state from a memento.
public sealed class DocumentEditor
{
public string Content { get; private set; }
public int CursorPosition { get; private set; }
public bool IsBold { get; private set; }
public DocumentEditor()
: this(string.Empty, 0, false)
{
}
public DocumentEditor(
string content,
int cursorPosition,
bool isBold)
{
Content = content;
CursorPosition = cursorPosition;
IsBold = isBold;
}
public void TypeText(string text)
{
Content = Content.Insert(CursorPosition, text);
CursorPosition += text.Length;
}
public void DeleteBackward(int count)
{
if (CursorPosition < count)
{
count = CursorPosition;
}
Content = Content.Remove(
CursorPosition - count, count);
CursorPosition -= count;
}
public void MoveCursor(int position)
{
CursorPosition = Math.Clamp(
position, 0, Content.Length);
}
public void ToggleBold()
{
IsBold = !IsBold;
}
public EditorMemento CreateMemento()
{
return new EditorMemento(
Content, CursorPosition, IsBold);
}
public void RestoreMemento(EditorMemento memento)
{
Content = memento.Content;
CursorPosition = memento.CursorPosition;
IsBold = memento.IsBold;
}
}
The CreateMemento method captures a complete snapshot. The RestoreMemento method overwrites all state from a memento. Notice that the editor doesn't manage any history -- it just knows how to snapshot and restore. This is the core separation the memento pattern provides.
The editing operations (TypeText, DeleteBackward, MoveCursor, ToggleBold) mutate state directly. They don't know about undo, redo, or history. That responsibility belongs to the caretaker. This separation is what makes the memento pattern powerful in a real-world C# application -- each component stays focused on a single job.
Compare this approach with how the template method pattern delegates specific steps to subclasses. In both patterns, the key insight is defining clear boundaries between what changes and what stays stable.
If you're familiar with how the state pattern manages transitions between behavioral modes, the memento pattern addresses a different concern entirely. The state pattern controls what an object can do. The memento pattern controls what an object remembers.
Creating the EditorHistory as the Caretaker
The caretaker manages memento storage and orchestrates undo/redo. It never inspects memento contents -- it just holds references and hands them back to the originator when needed.
public sealed class EditorHistory
{
private readonly Stack<EditorMemento> _undoStack = new();
private readonly Stack<EditorMemento> _redoStack = new();
public bool CanUndo => _undoStack.Count > 0;
public bool CanRedo => _redoStack.Count > 0;
public int UndoCount => _undoStack.Count;
public int RedoCount => _redoStack.Count;
public void Save(EditorMemento memento)
{
_undoStack.Push(memento);
_redoStack.Clear();
}
public EditorMemento Undo()
{
if (!CanUndo)
{
throw new InvalidOperationException(
"Nothing to undo.");
}
return _undoStack.Pop();
}
public void SaveForRedo(EditorMemento memento)
{
_redoStack.Push(memento);
}
public EditorMemento Redo()
{
if (!CanRedo)
{
throw new InvalidOperationException(
"Nothing to redo.");
}
return _redoStack.Pop();
}
}
The caretaker follows a deliberate protocol. Calling Save pushes a new memento onto the undo stack and clears redo -- matching standard editor behavior where new edits invalidate the redo chain. The Undo and Redo methods return mementos without restoring them. This keeps the caretaker decoupled from the originator, which aligns with inversion of control principles where components depend on abstractions rather than concrete implementations.
Wiring the Memento Pattern Together
With all three memento pattern components built, we need a coordinator that orchestrates the workflow. The EditorSession class ties the originator and caretaker together:
public sealed class EditorSession
{
private readonly DocumentEditor _editor;
private readonly EditorHistory _history;
public EditorSession()
: this(new DocumentEditor(), new EditorHistory())
{
}
public EditorSession(
DocumentEditor editor,
EditorHistory history)
{
_editor = editor;
_history = history;
}
public string Content => _editor.Content;
public int CursorPosition => _editor.CursorPosition;
public bool IsBold => _editor.IsBold;
public bool CanUndo => _history.CanUndo;
public bool CanRedo => _history.CanRedo;
public void TypeText(string text)
{
SaveCurrentState();
_editor.TypeText(text);
}
public void DeleteBackward(int count)
{
SaveCurrentState();
_editor.DeleteBackward(count);
}
public void MoveCursor(int position)
{
SaveCurrentState();
_editor.MoveCursor(position);
}
public void ToggleBold()
{
SaveCurrentState();
_editor.ToggleBold();
}
public void Undo()
{
if (!_history.CanUndo)
{
return;
}
// Save current state for redo before restoring
var currentMemento = _editor.CreateMemento();
_history.SaveForRedo(currentMemento);
var memento = _history.Undo();
_editor.RestoreMemento(memento);
}
public void Redo()
{
if (!_history.CanRedo)
{
return;
}
// Save current state for undo before restoring
var currentMemento = _editor.CreateMemento();
_history.Save(currentMemento);
var memento = _history.Redo();
_editor.RestoreMemento(memento);
}
private void SaveCurrentState()
{
var memento = _editor.CreateMemento();
_history.Save(memento);
}
}
Every mutating operation calls SaveCurrentState before making changes. This means the undo stack always holds the state from before the most recent edit. The Undo method saves the current state to the redo stack before restoring, so the user can redo back to where they were.
This coordination logic is where the memento pattern pays off. Adding new editing operations requires only a new method on DocumentEditor and a corresponding method on EditorSession that calls SaveCurrentState first. No changes to the history management, no changes to the memento class.
Unit Tests for Undo and Redo Behavior
Testing a memento pattern implementation is straightforward because each component has clear boundaries. Let's start with the originator:
public sealed class DocumentEditorTests
{
[Fact]
public void CreateMemento_CapturesAllState()
{
var editor = new DocumentEditor(
"Hello", 3, true);
var memento = editor.CreateMemento();
editor.TypeText(" World");
editor.RestoreMemento(memento);
Assert.Equal("Hello", editor.Content);
Assert.Equal(3, editor.CursorPosition);
Assert.True(editor.IsBold);
}
[Fact]
public void TypeText_InsertsAtCursorPosition()
{
var editor = new DocumentEditor(
"Hllo", 1, false);
editor.TypeText("e");
Assert.Equal("Hello", editor.Content);
Assert.Equal(2, editor.CursorPosition);
}
[Fact]
public void DeleteBackward_RemovesBeforeCursor()
{
var editor = new DocumentEditor(
"Hello", 5, false);
editor.DeleteBackward(3);
Assert.Equal("He", editor.Content);
Assert.Equal(2, editor.CursorPosition);
}
}
Now test the full undo/redo workflow through the session. These tests validate that the memento pattern correctly preserves and restores compound state:
public sealed class EditorSessionTests
{
[Fact]
public void Undo_RestoresPreviousState()
{
var session = new EditorSession();
session.TypeText("Hello");
session.TypeText(" World");
session.Undo();
Assert.Equal("Hello", session.Content);
Assert.Equal(5, session.CursorPosition);
}
[Fact]
public void Redo_ReappliesUndoneChange()
{
var session = new EditorSession();
session.TypeText("Hello");
session.Undo();
session.Redo();
Assert.Equal("Hello", session.Content);
}
[Fact]
public void NewEdit_AfterUndo_ClearsRedoStack()
{
var session = new EditorSession();
session.TypeText("First");
session.Undo();
session.TypeText("Second");
Assert.False(session.CanRedo);
Assert.Equal("Second", session.Content);
}
[Fact]
public void Undo_RestoresCursorAndFormatting()
{
var session = new EditorSession();
session.TypeText("Hello");
session.ToggleBold();
session.Undo();
Assert.False(session.IsBold);
Assert.Equal("Hello", session.Content);
}
[Fact]
public void MultipleUndos_WalkBackThroughHistory()
{
var session = new EditorSession();
session.TypeText("A");
session.TypeText("B");
session.TypeText("C");
session.Undo();
session.Undo();
session.Undo();
Assert.Equal(string.Empty, session.Content);
}
}
Each test targets a specific memento pattern behavior. The compound state test verifies that cursor position and formatting restore alongside content -- something that would break if the memento missed a field.
Limiting History Size for Memory Management
Unbounded history means unbounded memory consumption. Every memento stores a full copy of the document content. For large documents, this adds up fast. A bounded history caretaker enforces a maximum number of snapshots:
public sealed class BoundedEditorHistory
{
private readonly int _maxSize;
private readonly LinkedList<EditorMemento> _undoList = new();
private readonly Stack<EditorMemento> _redoStack = new();
public BoundedEditorHistory(int maxSize)
{
if (maxSize < 1)
{
throw new ArgumentOutOfRangeException(
nameof(maxSize),
"Max size must be at least 1.");
}
_maxSize = maxSize;
}
public bool CanUndo => _undoList.Count > 0;
public bool CanRedo => _redoStack.Count > 0;
public int UndoCount => _undoList.Count;
public void Save(EditorMemento memento)
{
if (_undoList.Count >= _maxSize)
{
// Drop the oldest memento
_undoList.RemoveFirst();
}
_undoList.AddLast(memento);
_redoStack.Clear();
}
public EditorMemento Undo()
{
if (!CanUndo)
{
throw new InvalidOperationException(
"Nothing to undo.");
}
var memento = _undoList.Last!.Value;
_undoList.RemoveLast();
return memento;
}
public void SaveForRedo(EditorMemento memento)
{
_redoStack.Push(memento);
}
public EditorMemento Redo()
{
if (!CanRedo)
{
throw new InvalidOperationException(
"Nothing to redo.");
}
return _redoStack.Pop();
}
}
The key change is LinkedList<EditorMemento> instead of Stack. A linked list lets us efficiently drop the oldest entry from the front when the history reaches capacity while still adding and removing from the back. This gives you a sliding window of undo history rather than unlimited growth.
Here's a test that validates the bounded behavior:
public sealed class BoundedEditorHistoryTests
{
[Fact]
public void Save_BeyondMaxSize_DropsOldestMemento()
{
var history = new BoundedEditorHistory(
maxSize: 2);
var editor = new DocumentEditor();
editor.TypeText("A");
history.Save(editor.CreateMemento());
editor.TypeText("B");
history.Save(editor.CreateMemento());
editor.TypeText("C");
history.Save(editor.CreateMemento());
// Only 2 mementos retained
Assert.Equal(2, history.UndoCount);
}
}
For production applications, you might configure the max history size through dependency injection using an options pattern. This makes the limit configurable per environment without changing code.
Production Considerations for the Memento Pattern
Moving this memento pattern example into production raises several practical questions. Here are the areas you'll need to address:
- Serialization -- persisting mementos across sessions
- Async operations -- capturing snapshots without blocking the UI
- Large documents -- controlling memory when state objects grow
- Hybrid undo -- combining mementos with command-based undo
- API complexity -- keeping consumer-facing interfaces simple
Serialization for persistence. If users expect to close and reopen a document with undo history intact, you need to serialize mementos. The record-based EditorMemento works well with System.Text.Json or any other serializer. Make properties public for serialization scenarios, or use a custom converter. Store the memento stack as a JSON array alongside the document file.
Async save operations. In a desktop or web editor, auto-save should run on a background thread. You can capture a memento on the UI thread (which is fast -- it's just copying fields) and then serialize it asynchronously. The memento pattern naturally supports this because the snapshot is an immutable object that's safe to pass across threads.
Large document optimization. When documents grow to hundreds of kilobytes, storing full copies in every memento gets expensive. Two common solutions exist. First, you can implement incremental mementos that store diffs between states instead of full snapshots -- this trades simplicity for space efficiency. Second, you can compress memento content using GZipStream before storage.
Combining with the command pattern. In complex editors, mementos and commands complement each other. Use the command pattern for granular operations that are easy to reverse (insert, delete), and use the memento pattern for operations where computing the inverse is difficult or impossible (bulk reformatting, paste with style). The strategy pattern can help select between these approaches at runtime based on operation type. You might even wrap the undo strategy selection behind a proxy that lazily loads heavier memento snapshots only when the user actually invokes undo.
Simplifying complex APIs. If you find that consumers of your editor need to interact with multiple subsystems to set up the memento pattern workflow, consider wrapping the coordination behind a facade. The EditorSession class in this article already functions as a lightweight facade over the originator and caretaker.
Frequently Asked Questions
When should I use the memento pattern instead of the command pattern for undo?
Use the memento pattern when operations are complex, hard to invert, or when the state is small relative to the number of operations. The command pattern works better when operations are simple to reverse and you want granular undo without full snapshots. Many editors combine both approaches depending on the operation type.
How much memory does the memento pattern consume?
Each memento stores a complete copy of the originator's state. For our document editor, that's the full text string, an integer, and a boolean per snapshot. With 100 undo levels and a 10 KB document, that's roughly 1 MB. Use bounded history and consider compression for larger state objects.
Can I use the memento pattern with Entity Framework or database-backed state?
Yes. Capture entity state into a memento before modifications, then restore by overwriting entity properties from the memento. Be careful with navigation properties and change tracking -- you may need to call DbContext.Entry(entity).CurrentValues.SetValues() rather than replacing the entire object.
How do I make the memento pattern thread-safe in C#?
The memento itself is immutable, so it's already thread-safe. For the caretaker, wrap Save, Undo, and Redo operations with a lock or use ConcurrentStack<T>. If the originator's CreateMemento reads multiple fields, ensure those reads are atomic -- either lock the originator or use a snapshot mechanism like copying to local variables first.
Should I use the memento pattern for configuration rollback?
Configuration rollback is one of the strongest use cases for the memento pattern in C#. Before applying new settings, capture a memento. If validation fails or the user cancels, restore from the memento. This avoids tracking which specific fields changed and eliminates partial rollback bugs.
Can the memento pattern work with immutable state objects?
When your state is already immutable -- like C# records -- the memento becomes trivial because you just store a reference to the existing state object. No deep copying needed. This is the most efficient form of the memento pattern because creating and restoring mementos have zero allocation overhead beyond the reference.
What's the difference between the memento pattern and event sourcing?
Event sourcing stores every state change as an event and rebuilds state by replaying events. The memento pattern stores complete state snapshots at specific points. Event sourcing gives you a complete audit trail and the ability to rebuild state at any point. The memento pattern is simpler, doesn't require event replay infrastructure, and works well for localized undo/redo rather than system-wide state reconstruction.
Wrapping Up This Memento Pattern Real-World Example
This memento pattern real-world example in C# demonstrated the full lifecycle of snapshot-based undo/redo in a document editor. We built an EditorMemento that captures text, cursor, and formatting state. The DocumentEditor originator creates and restores mementos without leaking its internals. The EditorHistory caretaker manages undo and redo stacks without inspecting memento contents. The BoundedEditorHistory variant enforces memory limits by dropping the oldest snapshots.
The memento pattern works best when state is moderate in size and operations are difficult to reverse individually. Swap the DocumentEditor for your own domain object -- a configuration panel, a drawing canvas, a form wizard -- and the same originator/memento/caretaker structure applies. Start with the unbounded history for simplicity, add the bounded variant when memory profiling tells you to, and combine with the command pattern when your editor needs both snapshot-based and delta-based undo.

