Learnixo

.NET & C# Development · Lesson 47 of 229

Memento — Capture and Restore State

Memento — Capture and Restore State

The Memento pattern captures and externalises an object's internal state so it can be restored later — without violating encapsulation. The object being saved doesn't expose its internals; it produces an opaque snapshot.


Core Implementation

C#
// Memento — the snapshot (opaque to everyone except Originator)
public class DocumentMemento
{
    internal string Content    { get; }
    internal int    CursorPos  { get; }
    internal DateTime SavedAt  { get; }

    internal DocumentMemento(string content, int cursorPos)
    {
        Content   = content;
        CursorPos = cursorPos;
        SavedAt   = DateTime.UtcNow;
    }

    public override string ToString()
        => $"Snapshot at {SavedAt:HH:mm:ss} ({Content.Length} chars)";
}

// Originator — the object whose state we save/restore
public class TextDocument
{
    private string _content = "";
    private int    _cursor  = 0;

    public void Type(string text)
    {
        _content = _content.Insert(_cursor, text);
        _cursor  += text.Length;
    }

    public void MoveCursor(int position)
        => _cursor = Math.Clamp(position, 0, _content.Length);

    public string GetContent() => _content;

    // Save — produces memento (internal constructor keeps fields hidden)
    public DocumentMemento Save()
        => new(_content, _cursor);

    // Restore — applies memento back (internal access)
    public void Restore(DocumentMemento memento)
    {
        _content = memento.Content;
        _cursor  = memento.CursorPos;
    }

    public override string ToString()
        => $"[cursor:{_cursor}] {_content}";
}

// Caretaker — stores mementos without knowing their contents
public class UndoManager
{
    private readonly Stack<DocumentMemento> _history = new();
    private readonly Stack<DocumentMemento> _redoStack = new();

    public void SaveState(TextDocument doc)
    {
        _history.Push(doc.Save());
        _redoStack.Clear();   // new action invalidates redo history
    }

    public void Undo(TextDocument doc)
    {
        if (!_history.TryPop(out var memento)) return;
        _redoStack.Push(doc.Save());
        doc.Restore(memento);
    }

    public void Redo(TextDocument doc)
    {
        if (!_redoStack.TryPop(out var memento)) return;
        _history.Push(doc.Save());
        doc.Restore(memento);
    }
}

// Usage
var doc   = new TextDocument();
var undo  = new UndoManager();

undo.SaveState(doc);
doc.Type("Hello");
undo.SaveState(doc);
doc.Type(", World");
undo.SaveState(doc);
doc.Type("!");

Console.WriteLine(doc);   // [cursor:14] Hello, World!

undo.Undo(doc);
Console.WriteLine(doc);   // [cursor:12] Hello, World

undo.Undo(doc);
Console.WriteLine(doc);   // [cursor:5] Hello

undo.Redo(doc);
Console.WriteLine(doc);   // [cursor:12] Hello, World

Configuration Snapshot Pattern

C#
// Useful for "try and revert" scenarios
public class DatabaseConfiguration
{
    public string ConnectionString { get; set; } = "";
    public int    MaxPoolSize      { get; set; } = 100;
    public int    TimeoutSeconds   { get; set; } = 30;

    public ConfigSnapshot CreateSnapshot()
        => new(ConnectionString, MaxPoolSize, TimeoutSeconds);

    public void RestoreFrom(ConfigSnapshot snapshot)
    {
        ConnectionString = snapshot.ConnectionString;
        MaxPoolSize      = snapshot.MaxPoolSize;
        TimeoutSeconds   = snapshot.TimeoutSeconds;
    }
}

public record ConfigSnapshot(string ConnectionString, int MaxPoolSize, int TimeoutSeconds);

// Test new config, revert if it fails
var config   = new DatabaseConfiguration { MaxPoolSize = 100 };
var snapshot = config.CreateSnapshot();

config.MaxPoolSize = 500;   // try new value

if (!await TestConnectionAsync(config))
    config.RestoreFrom(snapshot);   // revert on failure

Modern C# with Records

C#
// Immutable records make memento trivial — the record IS the memento
public record GameState(
    int     Level,
    int     Score,
    int     Lives,
    (int X, int Y) PlayerPosition
);

public class Game
{
    public GameState State { get; private set; } = new(1, 0, 3, (0, 0));
    private readonly Stack<GameState> _saves = new();

    public void Save()   => _saves.Push(State);
    public void Load()   { if (_saves.TryPop(out var s)) State = s; }

    public void Move(int dx, int dy)
    {
        var (x, y) = State.PlayerPosition;
        State = State with { PlayerPosition = (x + dx, y + dy) };
    }
}

Interview Answer

"Memento captures an object's internal state into an opaque snapshot without exposing private fields — the Originator creates the memento (has access to internals), the Caretaker stores it (doesn't know what's inside), and the Originator restores from it. Classic uses: undo/redo in text editors, configuration rollback (save before applying changes, restore on failure), and game save points. In modern C#, immutable records simplify Memento significantly — a record instance IS the snapshot; create a new record with with { } for mutations and keep the old record for undo. The pattern's key benefit over directly copying state externally is that the Originator controls what's saved and how — callers cannot read or manipulate the saved state."