Programmazione

Command Pattern in C#: guida completa con undo, redo e Dependency Injection

Dario Fadda Aprile 19, 2026

Il Command Pattern è uno dei design pattern comportamentali più utili nel mondo dello sviluppo software. In C#, la sua implementazione è particolarmente elegante e consente di costruire sistemi robusti con funzionalità di undo/redo, accodamento di operazioni e logging senza intaccare la logica di business. In questo articolo vedremo come implementarlo passo dopo passo, con esempi concreti e consigli pratici per applicazioni reali.

Cos’è il Command Pattern?

Il Command Pattern incapsula una richiesta come un oggetto autonomo, separando chi la emette da chi la esegue. Questo consente di parametrizzare i client con richieste diverse, accodare operazioni, supportare il rollback e costruire sistemi di macro o audit trail. In parole semplici: invece di chiamare direttamente un metodo, si crea un oggetto che “sa come” eseguire quella chiamata, e lo si passa all’esecutore.

I casi d’uso più classici includono:

  • Editor di testo o grafica con undo/redo illimitato
  • Sistemi di workflow con operazioni reversibili
  • Transaction scripts in architetture DDD
  • Logging e auditing di operazioni critiche
  • Macro recording nelle applicazioni di produttività

I componenti del pattern

Un’implementazione corretta del Command Pattern in C# prevede quattro attori principali:

  • ICommand: l’interfaccia contrattuale che definisce Execute(), Undo() e una proprietà descrittiva
  • Receiver: la classe che contiene la logica di business effettiva, ignara del pattern
  • Concrete Command: le implementazioni di ICommand, che catturano i parametri in costruzione e delegano al Receiver
  • Invoker: gestisce la coda di esecuzione e le stack di undo/redo

Implementazione passo dopo passo

Step 1: definire l’interfaccia ICommand

Il contratto deve essere minimale. Tutti i dati necessari all’esecuzione viaggiano dentro il comando stesso, non vengono passati ai metodi:

public interface ICommand
{
    string Description { get; }
    void Execute();
    void Undo();
}

La scelta di metodi senza parametri è deliberata: favorisce l’immutabilità del comando dopo la costruzione e impedisce il condizionamento da stato esterno.

Step 2: creare il Receiver

Il Receiver contiene la logica reale. Non sa nulla di comandi, stack o undo. È testabile in isolamento:

public class TextDocument
{
    private readonly List<string> _lines = new();

    public IReadOnlyList<string> Lines => _lines.AsReadOnly();

    public void AddLine(string text) => _lines.Add(text);

    public void RemoveLine(int index)
    {
        if (index >= 0 && index < _lines.Count)
            _lines.RemoveAt(index);
    }
}

Step 3: implementare i Concrete Commands

Ogni comando cattura i propri dati al momento della costruzione e implementa sia Execute che Undo. Notare che lo stato necessario per l’undo viene salvato durante Execute:

public sealed class AddLineCommand : ICommand
{
    private readonly TextDocument _document;
    private readonly string _text;

    public string Description => $"Aggiungi riga: "{_text}"";

    public AddLineCommand(TextDocument document, string text)
    {
        _document = document ?? throw new ArgumentNullException(nameof(document));
        _text = text ?? throw new ArgumentNullException(nameof(text));
    }

    public void Execute() => _document.AddLine(_text);

    public void Undo() => _document.RemoveLine(_document.Lines.Count - 1);
}

public sealed class RemoveLineCommand : ICommand
{
    private readonly TextDocument _document;
    private readonly int _index;
    private string? _removedText;

    public string Description => $"Rimuovi riga {_index}";

    public RemoveLineCommand(TextDocument document, int index)
    {
        _document = document;
        _index = index;
    }

    public void Execute()
    {
        // Salviamo lo stato per poter fare undo
        _removedText = _document.Lines[_index];
        _document.RemoveLine(_index);
    }

    public void Undo()
    {
        if (_removedText is not null)
            _document.AddLine(_removedText);
    }
}

Il punto critico è la cattura dello snapshot in Execute(): RemoveLineCommand ricorda il testo rimosso prima di cancellarlo, rendendo possibile il ripristino.

Step 4: costruire l’Invoker con history

L’Invoker mantiene due stack: uno per l’undo e uno per il redo. Quando si esegue un nuovo comando, la redo stack viene svuotata per evitare storie ramificate incoerenti:

public class CommandInvoker
{
    private readonly Stack<ICommand> _undoStack = new();
    private readonly Stack<ICommand> _redoStack = new();

    public void ExecuteCommand(ICommand command)
    {
        command.Execute();
        _undoStack.Push(command);
        _redoStack.Clear(); // Nuova azione invalida il redo
    }

    public bool CanUndo => _undoStack.Count > 0;
    public bool CanRedo => _redoStack.Count > 0;

    public void Undo()
    {
        if (!CanUndo) return;
        var cmd = _undoStack.Pop();
        cmd.Undo();
        _redoStack.Push(cmd);
    }

    public void Redo()
    {
        if (!CanRedo) return;
        var cmd = _redoStack.Pop();
        cmd.Execute();
        _undoStack.Push(cmd);
    }

    public IEnumerable<string> GetHistory() =>
        _undoStack.Select(c => c.Description);
}

Step 5: Macro Commands (Composite)

Il Command Pattern si combina naturalmente con il Composite Pattern per costruire macro che raggruppano più operazioni atomiche:

public sealed class MacroCommand : ICommand
{
    private readonly IReadOnlyList<ICommand> _commands;

    public string Description => $"Macro ({_commands.Count} operazioni)";

    public MacroCommand(IEnumerable<ICommand> commands)
    {
        _commands = commands.ToList();
    }

    public void Execute()
    {
        foreach (var cmd in _commands)
            cmd.Execute();
    }

    public void Undo()
    {
        // L'undo avviene in ordine inverso
        for (int i = _commands.Count - 1; i >= 0; i--)
            _commands[i].Undo();
    }
}

Step 6: integrazione con Dependency Injection

In applicazioni .NET moderne, il pattern si integra perfettamente con il DI container di ASP.NET Core:

// Program.cs
builder.Services.AddSingleton<TextDocument>();
builder.Services.AddSingleton<CommandInvoker>();
builder.Services.AddTransient<DocumentCommandFactory>();

// Factory per creare comandi on-demand
public class DocumentCommandFactory
{
    private readonly TextDocument _document;

    public DocumentCommandFactory(TextDocument document)
    {
        _document = document;
    }

    public ICommand AddLine(string text) => new AddLineCommand(_document, text);
    public ICommand RemoveLine(int index) => new RemoveLineCommand(_document, index);
}

I componenti longevi (TextDocument, CommandInvoker) sono Singleton; i comandi vengono creati on-demand tramite factory e rimangono short-lived.

Errori comuni da evitare

Chi si avvicina al Command Pattern per la prima volta tende a commettere questi errori:

  • Logica nel comando invece che nel Receiver: i comandi devono orchestrare, non implementare. La logica di business appartiene al Receiver, dove può essere testata in isolamento.
  • Mancato snapshot dello stato per l’undo: se dimenticate di catturare lo stato prima dell’esecuzione, l’undo diventa impossibile o inaffidabile.
  • Stato condiviso tra comandi: ogni comando deve essere autosufficiente. Lo stato mutabile condiviso porta a bug sottili quando i comandi vengono eseguiti in ordine diverso.
  • Undo implementato in modo forzato: per operazioni genuinamente non reversibili (come l’invio di un’email), meglio lanciare un’eccezione o documentare esplicitamente il limite, piuttosto che fingere un undo inesistente.

Conclusione

Il Command Pattern è uno strumento potente e sottovalutato. Quando il vostro sistema ha bisogno di storico operazioni, undo/redo, accodamento differito o audit trail, questo pattern vi offre una soluzione elegante e testabile. La separazione netta tra Receiver (logica) e Command (orchestrazione) rende il codice mantenibile anche su larga scala.

Se state sviluppando con C# e .NET, consideratelo ogni volta che la vostra architettura richiede operazioni reversibili o la composizione di azioni complesse.


Fonte originale: How to Implement Command Pattern in C# – DevLeader.ca

💬 Unisciti alla discussione!


Questo è un blog del Fediverso: puoi trovare quindi questo articolo ovunque con @blog@spcnet.it e ogni commento/risposta apparirà qui sotto.

Se vuoi commentare su Command Pattern in C#: guida completa con undo, redo e Dependency Injection, utilizza la discussione sul Forum.
Condividi la tua esperienza, confrontati con altri professionisti e approfondisci i dettagli tecnici nel nostro 👉 forum community