Memento

Memento

Save points in a video game are Mementos. Before a boss fight, the game captures your exact state — health, inventory, position, quest progress — into a save file. If you die, you restore from the save point and try again. The save file captures everything needed to recreate the moment without exposing the game’s internal data structures to the save system.

The Memento pattern captures and externalizes an object’s internal state so it can be restored later, without violating encapsulation. Three participants: the originator (your shopping cart) creates a memento containing a snapshot of its state. The caretaker (the history manager) stores mementos without inspecting their contents. When undo is needed, the originator restores itself from a memento. The caretaker never accesses or modifies the saved state — it just holds the opaque snapshots.

sequenceDiagram
    participant Cart as ShoppingCart
    participant Memento as CartMemento
    participant History as CartHistory
    Cart->>Memento: CreateMemento with current state
    Memento->>History: Store snapshot
    Note over Cart: User modifies cart
    Cart->>History: Request undo
    History->>Memento: Get last snapshot
    Memento->>Cart: Restore previous state

Problem

A shopping cart has no undo. Removing an item by accident is permanent, and abandoned cart recovery requires external DB snapshots with no clean abstraction:

public class ShoppingCart
{
    public List<CartItem> Items { get; set; } = [];
    public string? DiscountCode { get; set; }
    public decimal Total => Items.Sum(i => i.Price * i.Quantity);

    // ⚠️ No way to undo — once an item is removed, it's gone
    public void RemoveItem(Guid productId) =>
        Items.RemoveAll(i => i.ProductId == productId);

    // ⚠️ No snapshot capability — abandoned cart recovery requires external logic
}

public class CartController
{
    public IActionResult RemoveItem(Guid productId)
    {
        _cart.RemoveItem(productId);
        // ⚠️ Customer immediately regrets this — no undo button
        return Ok();
    }
}

Here's what breaks when requirements change: adding "undo last change" requires retrofitting state tracking into the cart — a significant refactor.

Solution

CartMemento captures cart state; CartHistory stores snapshots:

// Memento — immutable snapshot of cart state
public sealed record CartMemento(
    IReadOnlyList<CartItem> Items,
    string? DiscountCode,
    DateTime SnapshotAt);

// Originator — creates and restores from mementos
public class ShoppingCart
{
    private List<CartItem> _items = [];
    public string? DiscountCode { get; private set; }
    public IReadOnlyList<CartItem> Items => _items.AsReadOnly();
    public decimal Total => _items.Sum(i => i.Price * i.Quantity);

    public void AddItem(CartItem item) => _items.Add(item);
    public void RemoveItem(Guid productId) => _items.RemoveAll(i => i.ProductId == productId);
    public void ApplyDiscount(string code) => DiscountCode = code;

    // ✅ Creates a snapshot of current state
    public CartMemento Save() =>
        new(_items.Select(i => i with { }).ToList().AsReadOnly(), DiscountCode, DateTime.UtcNow);

    // ✅ Restores state from a snapshot
    public void Restore(CartMemento memento)
    {
        _items = memento.Items.Select(i => i with { }).ToList(); // deep copy
        DiscountCode = memento.DiscountCode;
    }
}

// Caretaker — stores mementos without inspecting them
public class CartHistory
{
    private readonly Stack<CartMemento> _history = new();

    public void Push(CartMemento memento) => _history.Push(memento);

    public CartMemento? Pop() => _history.TryPop(out var m) ? m : null;

    public bool CanUndo => _history.Count > 0;

    // ✅ Serialize for abandoned cart recovery
    public string Serialize() => JsonSerializer.Serialize(_history.ToArray());
    public static CartHistory Deserialize(string json)
    {
        var history = new CartHistory();
        var mementos = JsonSerializer.Deserialize<CartMemento[]>(json) ?? [];
        foreach (var m in mementos.Reverse()) history.Push(m);
        return history;
    }
}

// Usage
var cart = new ShoppingCart();
var history = new CartHistory();

cart.AddItem(new CartItem(laptopId, 1, 1299m));
history.Push(cart.Save()); // ✅ snapshot before change

cart.RemoveItem(laptopId); // customer removes item

// Customer clicks "Undo"
if (history.CanUndo)
    cart.Restore(history.Pop()!); // ✅ laptop is back

Abandoned cart recovery now uses CartHistory.Serialize() — the same snapshot mechanism, no separate DB schema needed.

You Already Use This

EF Core ChangeTracker.OriginalValues — EF Core stores the original database values for each tracked entity. entry.OriginalValues["Total"] returns the value before any in-memory changes. entry.CurrentValues.SetValues(entry.OriginalValues) restores the entity to its original state — a Memento restore.

JSON serialization as state snapshotJsonSerializer.Serialize(cart) captures the cart state as a string. JsonSerializer.Deserialize<ShoppingCart>(json) restores it. This is the Memento pattern with JSON as the memento format — used for abandoned cart recovery, session state, and event sourcing snapshots.

DataSet.GetChanges() / RejectChanges()DataSet.GetChanges() returns a memento of all modified rows. RejectChanges() restores the dataset to its original state.

Questions

References


Whats next