Turning Actions Into Objects
If you've ever needed to implement undo/redo in an application, you've felt the pain of trying to reverse operations after they've already executed. Direct method calls vanish once they complete—there's no object to store in a history stack, no way to capture the operation's context for later reversal. The Command pattern solves this by turning requests into first-class objects that can be queued, logged, undone, and replayed.
By encapsulating a request as an object, you decouple the invoker (who triggers the action) from the receiver (who performs it). This separation enables powerful capabilities: you can pass commands as parameters, store them in collections, execute them later, or reverse their effects. It's the foundation of undo systems, macro recording, transaction logs, and job queues.
You'll learn how to implement the core Command interface, build undo/redo functionality, queue commands for batch execution, and handle complex scenarios like composite commands and parameterized operations that maintain full reversibility.
The Core Command Interface
The Command pattern starts with a simple interface that declares an Execute method. Each concrete command implements this interface, encapsulating a specific operation along with its parameters. The command object stores everything needed to perform the action, making the request self-contained and reusable.
This design lets you treat all operations uniformly. Whether you're printing a document, saving a file, or updating a database, each command exposes the same Execute interface. Invokers work with the abstraction, never knowing the concrete operation they're triggering.
// Command interface
public interface ICommand
{
void Execute();
}
// Receiver - the object that performs the actual work
public class TextEditor
{
private readonly List<string> _lines = new();
public void InsertLine(string text)
{
_lines.Add(text);
Console.WriteLine($"Inserted: {text}");
}
public void DeleteLastLine()
{
if (_lines.Any())
{
var removed = _lines[^1];
_lines.RemoveAt(_lines.Count - 1);
Console.WriteLine($"Deleted: {removed}");
}
}
public void Display()
{
Console.WriteLine("\nCurrent content:");
foreach (var line in _lines)
Console.WriteLine($" {line}");
}
}
// Concrete command
public class InsertTextCommand : ICommand
{
private readonly TextEditor _editor;
private readonly string _text;
public InsertTextCommand(TextEditor editor, string text)
{
_editor = editor;
_text = text;
}
public void Execute()
{
_editor.InsertLine(_text);
}
}
public class DeleteTextCommand : ICommand
{
private readonly TextEditor _editor;
public DeleteTextCommand(TextEditor editor)
{
_editor = editor;
}
public void Execute()
{
_editor.DeleteLastLine();
}
}
Each command holds a reference to its receiver (the TextEditor) and any parameters needed for execution. The InsertTextCommand knows what text to insert, while DeleteTextCommand knows it operates on the last line. When Execute runs, the command delegates to the receiver's methods, but the command itself controls when and how that happens.
Implementing Undo and Redo
The real power of Command emerges when you add undo capability. Extend the interface with an Undo method, and have each command store the information needed to reverse its effect. A history manager maintains stacks of executed and undone commands, enabling full undo/redo navigation through your operation history.
The key is capturing enough state in Execute to make Undo possible. For insertion, you need to remember what was inserted so you can remove it. For deletion, you need to save what was deleted so you can restore it. Each command becomes a complete transaction with both forward and backward paths.
public interface IUndoableCommand
{
void Execute();
void Undo();
}
public class Document
{
private readonly List<string> _content = new();
public void AddText(string text)
{
_content.Add(text);
}
public void RemoveText(string text)
{
_content.Remove(text);
}
public string GetContent() => string.Join("\n", _content);
}
public class AddTextCommand : IUndoableCommand
{
private readonly Document _document;
private readonly string _text;
public AddTextCommand(Document document, string text)
{
_document = document;
_text = text;
}
public void Execute()
{
_document.AddText(_text);
Console.WriteLine($"Added: {_text}");
}
public void Undo()
{
_document.RemoveText(_text);
Console.WriteLine($"Undid add: {_text}");
}
}
public class CommandHistory
{
private readonly Stack<IUndoableCommand> _undoStack = new();
private readonly Stack<IUndoableCommand> _redoStack = new();
public void ExecuteCommand(IUndoableCommand command)
{
command.Execute();
_undoStack.Push(command);
_redoStack.Clear(); // New action invalidates redo history
}
public void Undo()
{
if (_undoStack.TryPop(out var command))
{
command.Undo();
_redoStack.Push(command);
}
else
{
Console.WriteLine("Nothing to undo");
}
}
public void Redo()
{
if (_redoStack.TryPop(out var command))
{
command.Execute();
_undoStack.Push(command);
}
else
{
Console.WriteLine("Nothing to redo");
}
}
}
The CommandHistory class orchestrates the undo/redo behavior. When you execute a command, it goes on the undo stack. When you undo, it moves to the redo stack after reversing. When you execute a new command after undoing, the redo stack clears—you can't redo something after the timeline has diverged. This pattern matches user expectations from every text editor and graphics program.
Composite Commands for Complex Operations
Some operations consist of multiple steps that should execute and undo as a single unit. Composite commands group several commands together, executing them in sequence and undoing them in reverse order. This enables macro functionality and transactional operations where all steps succeed or all fail together.
The Composite pattern combines naturally with Command. A composite command is itself a command—it implements ICommand—but internally manages a collection of child commands. When executed, it runs each child. When undone, it reverses each child in reverse order.
public class CompositeCommand : IUndoableCommand
{
private readonly List<IUndoableCommand> _commands = new();
private readonly string _description;
public CompositeCommand(string description)
{
_description = description;
}
public void Add(IUndoableCommand command)
{
_commands.Add(command);
}
public void Execute()
{
Console.WriteLine($"\n=== Executing: {_description} ===");
foreach (var command in _commands)
{
command.Execute();
}
}
public void Undo()
{
Console.WriteLine($"\n=== Undoing: {_description} ===");
// Undo in reverse order
for (int i = _commands.Count - 1; i >= 0; i--)
{
_commands[i].Undo();
}
}
}
// Usage example
var doc = new Document();
var history = new CommandHistory();
// Create a composite "format paragraph" command
var formatParagraph = new CompositeCommand("Format Paragraph");
formatParagraph.Add(new AddTextCommand(doc, "## Heading"));
formatParagraph.Add(new AddTextCommand(doc, "First sentence."));
formatParagraph.Add(new AddTextCommand(doc, "Second sentence."));
history.ExecuteCommand(formatParagraph);
Console.WriteLine($"\nDocument:\n{doc.GetContent()}");
history.Undo();
Console.WriteLine($"\nAfter undo:\n{doc.GetContent()}");
Composite commands let you treat complex multi-step operations as atomic units. A "format paragraph" operation might add a heading, insert text, and apply styling. Users see it as one action and expect one undo to reverse the entire operation. Composite makes this natural by grouping the steps and managing their reversal order automatically.
Queuing and Scheduling Commands
Commands as objects enable deferred execution. Instead of running operations immediately, you can queue them for later processing. This supports batch operations, scheduling, prioritization, and background execution. Commands become work items that flow through your system asynchronously.
A command queue decouples producers (who create commands) from consumers (who execute them). Producers don't block waiting for execution. Consumers process commands at their own pace, potentially on different threads or machines. This pattern underpins job queues, message brokers, and distributed task systems.
public class CommandQueue
{
private readonly Queue<ICommand> _commands = new();
public void Enqueue(ICommand command)
{
_commands.Enqueue(command);
Console.WriteLine($"Queued command (total: {_commands.Count})");
}
public void ProcessAll()
{
Console.WriteLine($"\nProcessing {_commands.Count} commands...");
while (_commands.Count > 0)
{
var command = _commands.Dequeue();
command.Execute();
}
Console.WriteLine("All commands processed\n");
}
public void ProcessBatch(int count)
{
Console.WriteLine($"\nProcessing batch of {count} commands...");
for (int i = 0; i < count && _commands.Count > 0; i++)
{
_commands.Dequeue().Execute();
}
Console.WriteLine($"Batch complete. Remaining: {_commands.Count}\n");
}
}
// Example: Background job processing
public class EmailCommand : ICommand
{
private readonly string _recipient;
private readonly string _subject;
public EmailCommand(string recipient, string subject)
{
_recipient = recipient;
_subject = subject;
}
public void Execute()
{
Console.WriteLine($"Sending email to {_recipient}: {_subject}");
Thread.Sleep(100); // Simulate work
}
}
Queuing separates what work needs doing from when it gets done. Email commands can be queued during request handling and processed by a background worker. Commands can be prioritized, retried on failure, or persisted to disk for crash recovery. The command object captures everything needed for execution, making it safe to defer the work until the right time or place.
Try It Yourself
Build a simple command system with undo/redo support to see the pattern in action.
Steps
- Scaffold project:
dotnet new console -n CommandDemo
- Change directory:
cd CommandDemo
- Replace Program.cs with code below
- Run the demo:
dotnet run
interface ICommand
{
void Execute();
void Undo();
}
class Calculator
{
public int Value { get; private set; }
public void Add(int amount) => Value += amount;
public void Subtract(int amount) => Value -= amount;
}
class AddCommand : ICommand
{
private readonly Calculator _calc;
private readonly int _amount;
public AddCommand(Calculator calc, int amount) { _calc = calc; _amount = amount; }
public void Execute() { _calc.Add(_amount); Console.WriteLine($"+{_amount} = {_calc.Value}"); }
public void Undo() { _calc.Subtract(_amount); Console.WriteLine($"Undo +{_amount} = {_calc.Value}"); }
}
class History
{
private readonly Stack<ICommand> _undo = new();
private readonly Stack<ICommand> _redo = new();
public void Execute(ICommand cmd) { cmd.Execute(); _undo.Push(cmd); _redo.Clear(); }
public void Undo()
{
if (_undo.TryPop(out var cmd)) { cmd.Undo(); _redo.Push(cmd); }
else Console.WriteLine("Nothing to undo");
}
public void Redo()
{
if (_redo.TryPop(out var cmd)) { cmd.Execute(); _undo.Push(cmd); }
else Console.WriteLine("Nothing to redo");
}
}
var calc = new Calculator();
var history = new History();
history.Execute(new AddCommand(calc, 10));
history.Execute(new AddCommand(calc, 5));
history.Execute(new AddCommand(calc, 3));
Console.WriteLine($"\nFinal value: {calc.Value}\n");
history.Undo();
history.Undo();
Console.WriteLine($"\nAfter 2 undos: {calc.Value}\n");
history.Redo();
Console.WriteLine($"\nAfter redo: {calc.Value}");
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
What you'll see
+10 = 10
+5 = 15
+3 = 18
Final value: 18
Undo +3 = 15
Undo +5 = 10
After 2 undos: 10
+5 = 15
After redo: 15
Mistakes to Avoid
Forgetting to clear redo stack: When executing a new command after undoing, you must clear the redo stack. Otherwise, users can redo operations that no longer make sense in the current timeline. Always clear redo on new executions to maintain history integrity.
Storing mutable state incorrectly: If your command stores a reference to mutable state for undo, changes to that state break undo. For example, storing a reference to a list doesn't help if the list contents change. Clone the state or store immutable snapshots. Each command must own the data needed for its undo operation.
Unbounded history growth: Every executed command consumes memory. Without limits, long-running applications exhaust RAM. Implement a maximum history depth and discard old commands, or use checkpointing where you save occasional snapshots and discard older individual commands. Balance undo capability against memory constraints.
Ignoring command failures: Commands can fail during execution or undo. Handle exceptions appropriately—log the failure, mark the command as faulted, or compensate with alternative actions. Don't let exceptions leave your application in an inconsistent state. Consider implementing a try-catch pattern in your command executor that handles failures gracefully.
Over-granular commands: Not every setter call needs to be a command. Fine-grained commands make undo tedious for users (imagine undoing every keystroke separately). Group related changes into meaningful units. A "rename variable" command should capture the entire rename, not individual character edits. Balance granularity with user expectations.