Command Pattern Real-World Example in C#: Complete Implementation
Most command pattern tutorials define an ICommand interface, create a LightOnCommand, and flip a switch. That's fine for a textbook, but it won't help you the next time you need undo/redo, macro recording, or an auditable command history. This article builds a complete command pattern real-world example in C# -- a text editor engine with insert, delete, format, undo/redo, and macro recording.
By the end, you'll have compilable classes covering the full evolution: problem, interface design, concrete commands, an invoker with undo/redo stacks, macro recording, unit tests, and DI registration. If you want to see how the command pattern fits alongside other behavioral patterns like strategy, this article gives you the practical foundation.
The Problem: Direct Method Calls Without Command Objects
You're building a text editor. Users need to type text, delete text, apply bold formatting, and undo their mistakes. Without the command pattern, the editor class ends up owning all the logic directly:
public class TextEditor
{
private string _content = string.Empty;
private readonly Stack<string> _undoSnapshots = new();
public void InsertText(int position, string text)
{
_undoSnapshots.Push(_content);
_content = _content.Insert(position, text);
}
public void DeleteText(int position, int length)
{
_undoSnapshots.Push(_content);
_content = _content.Remove(position, length);
}
public void ApplyBold(int position, int length)
{
_undoSnapshots.Push(_content);
var selected = _content.Substring(
position, length);
_content = _content.Remove(position, length)
.Insert(position, $"**{selected}**");
}
public void Undo()
{
if (_undoSnapshots.Count > 0)
{
_content = _undoSnapshots.Pop();
}
}
}
This approach has several problems. Undo stores full snapshots -- wasteful for large documents. There's no redo. Adding operations means modifying the editor class. Macro recording would require yet another layer of snapshot management. Testing individual operations means going through the editor's public API and inspecting string state.
The command pattern eliminates this by encapsulating each operation as an object. Each command knows how to execute and undo itself. An invoker manages command history, undo, and redo. Macros become a list of commands replayed in sequence.
Designing the Document Model
Before we build commands, we need a receiver -- the object that commands act upon. A simple Document class gives us the mutable state that commands will modify:
public sealed class Document
{
private string _content;
public Document()
: this(string.Empty)
{
}
public Document(string initialContent)
{
_content = initialContent;
}
public string Content => _content;
public int Length => _content.Length;
public void Insert(int position, string text)
{
_content = _content.Insert(position, text);
}
public void Remove(int position, int length)
{
_content = _content.Remove(position, length);
}
public string Substring(int position, int length)
{
return _content.Substring(position, length);
}
}
The Document class is intentionally simple. It provides primitive operations without any knowledge of undo, redo, or command history. The document is the receiver in command pattern terminology: it knows how to perform operations, but not when or why they're called. This separation connects to inversion of control principles in broader architecture.
Defining the Command Interface
The command pattern revolves around a single interface that every command implements. For our text editor, each command needs to execute its operation and undo it:
public interface ICommand
{
void Execute();
void Undo();
string Description { get; }
}
The Description property provides human-readable text for command history display. The interface is deliberately minimal. No Redo method is needed because redoing a command is the same as executing it again.
Building the InsertTextCommand
The first concrete command handles text insertion. It captures everything it needs to both execute and undo the operation:
public sealed class InsertTextCommand : ICommand
{
private readonly Document _document;
private readonly int _position;
private readonly string _text;
public InsertTextCommand(
Document document,
int position,
string text)
{
_document = document;
_position = position;
_text = text;
}
public string Description
=> $"Insert "{_text}" at position {_position}";
public void Execute()
{
_document.Insert(_position, _text);
}
public void Undo()
{
_document.Remove(_position, _text.Length);
}
}
Notice how the command captures the position and text at construction time. Undo is the exact inverse -- removing the same characters from the same position. No snapshots, no string copying. The command pattern gives us efficient undo by storing only the delta.
Building the DeleteTextCommand
Deletion is the inverse of insertion, but it needs to remember what was deleted so it can restore it:
public sealed class DeleteTextCommand : ICommand
{
private readonly Document _document;
private readonly int _position;
private readonly int _length;
private string _deletedText;
public DeleteTextCommand(
Document document,
int position,
int length)
{
_document = document;
_position = position;
_length = length;
_deletedText = string.Empty;
}
public string Description
=> $"Delete {_length} characters at position {_position}";
public void Execute()
{
_deletedText = _document.Substring(
_position, _length);
_document.Remove(_position, _length);
}
public void Undo()
{
_document.Insert(_position, _deletedText);
}
}
The _deletedText field captures the removed content during Execute so that Undo can re-insert it. This is a key command pattern technique -- commands often store state generated during execution to support reversal.
Building the FormatTextCommand
Formatting applies markup around a text range. This command wraps selected text with a configurable tag:
public sealed class FormatTextCommand : ICommand
{
private readonly Document _document;
private readonly int _position;
private readonly int _length;
private readonly string _openTag;
private readonly string _closeTag;
public FormatTextCommand(
Document document,
int position,
int length,
string openTag,
string closeTag)
{
_document = document;
_position = position;
_length = length;
_openTag = openTag;
_closeTag = closeTag;
}
public string Description
=> $"Format {_length} characters at position {_position} with {_openTag}";
public void Execute()
{
_document.Insert(
_position + _length, _closeTag);
_document.Insert(_position, _openTag);
}
public void Undo()
{
_document.Remove(_position, _openTag.Length);
_document.Remove(
_position + _length,
_closeTag.Length);
}
}
The Execute method inserts the close tag first, then the open tag. Order matters -- inserting the open tag first would shift the close tag's position. The Undo method reverses this by removing the open tag first, then the close tag. This works for bold (**), italic (*), HTML tags, or any paired delimiters.
Creating the CommandInvoker with Undo/Redo
The invoker is the heart of this command pattern implementation. It executes commands, maintains a history, and manages undo/redo stacks:
public sealed class CommandInvoker
{
private readonly Stack<ICommand> _undoStack = new();
private readonly Stack<ICommand> _redoStack = new();
private readonly List<ICommand> _history = [];
public IReadOnlyList<ICommand> History => _history;
public bool CanUndo => _undoStack.Count > 0;
public bool CanRedo => _redoStack.Count > 0;
public void Execute(ICommand command)
{
command.Execute();
_undoStack.Push(command);
_redoStack.Clear();
_history.Add(command);
}
public void Undo()
{
if (!CanUndo)
{
return;
}
var command = _undoStack.Pop();
command.Undo();
_redoStack.Push(command);
}
public void Redo()
{
if (!CanRedo)
{
return;
}
var command = _redoStack.Pop();
command.Execute();
_undoStack.Push(command);
}
}
Three design decisions worth highlighting. First, executing a new command clears the redo stack -- matching standard editor behavior. Second, the history list records every command ever executed, even after undo, giving you a complete audit trail. Third, redo simply calls Execute again on a previously undone command.
This invoker coordinates command execution across the application. If you're familiar with how the strategy pattern swaps behavior behind interfaces, think of the command pattern as the behavioral cousin that also captures when operations happen and how to reverse them.
Adding Macro Recording
A macro is a sequence of commands that can be replayed as a single unit. The command pattern makes this almost trivial -- a macro is itself a command that contains other commands. This approach aligns closely with how the composite pattern treats individual items and groups uniformly:
public sealed class MacroCommand : ICommand
{
private readonly List<ICommand> _commands;
public MacroCommand(IEnumerable<ICommand> commands)
{
_commands = commands.ToList();
}
public string Description
=> $"Macro ({_commands.Count} commands)";
public void Execute()
{
foreach (var command in _commands)
{
command.Execute();
}
}
public void Undo()
{
for (var i = _commands.Count - 1; i >= 0; i--)
{
_commands[i].Undo();
}
}
}
Execute runs commands in order. Undo runs them in reverse -- last executed, first undone. This is crucial for correctness. If a macro inserts text and then modifies a later position, undoing in forward order would corrupt the document.
Now we need a recorder that captures commands during a recording session:
public sealed class MacroRecorder
{
private List<ICommand>? _recording;
public bool IsRecording => _recording is not null;
public void StartRecording()
{
_recording = [];
}
public void RecordCommand(ICommand command)
{
_recording?.Add(command);
}
public MacroCommand StopRecording()
{
if (_recording is null)
{
throw new InvalidOperationException(
"No recording in progress.");
}
var macro = new MacroCommand(_recording);
_recording = null;
return macro;
}
}
The recorder collects commands while recording is active. Calling StopRecording produces a MacroCommand that can be stored, replayed, or passed to the invoker like any other command.
Wiring the Editor Together
With all the pieces built, let's create an EditorSession class that wires the document, invoker, and macro recorder into a cohesive API:
public sealed class EditorSession
{
private readonly Document _document;
private readonly CommandInvoker _invoker;
private readonly MacroRecorder _macroRecorder;
public EditorSession(
Document document,
CommandInvoker invoker,
MacroRecorder macroRecorder)
{
_document = document;
_invoker = invoker;
_macroRecorder = macroRecorder;
}
public string Content => _document.Content;
public bool CanUndo => _invoker.CanUndo;
public bool CanRedo => _invoker.CanRedo;
public bool IsRecording => _macroRecorder.IsRecording;
public IReadOnlyList<ICommand> History
=> _invoker.History;
public void InsertText(int position, string text)
{
var command = new InsertTextCommand(
_document, position, text);
ExecuteCommand(command);
}
public void DeleteText(int position, int length)
{
var command = new DeleteTextCommand(
_document, position, length);
ExecuteCommand(command);
}
public void FormatBold(int position, int length)
{
var command = new FormatTextCommand(
_document, position, length, "**", "**");
ExecuteCommand(command);
}
public void FormatItalic(int position, int length)
{
var command = new FormatTextCommand(
_document, position, length, "*", "*");
ExecuteCommand(command);
}
public void Undo() => _invoker.Undo();
public void Redo() => _invoker.Redo();
public void StartMacro()
=> _macroRecorder.StartRecording();
public MacroCommand StopMacro()
=> _macroRecorder.StopRecording();
public void PlayMacro(MacroCommand macro)
=> ExecuteCommand(macro);
private void ExecuteCommand(ICommand command)
{
_invoker.Execute(command);
_macroRecorder.RecordCommand(command);
}
}
The ExecuteCommand method is the central routing point. Every operation flows through it, which means every operation automatically gets undo/redo support and macro recording. That's the command pattern at its best -- cross-cutting concerns like history tracking become trivial when every operation is an object.
Testing the Command Pattern Implementation
Unit tests for a command pattern system are clean because each command is independently testable. Here are tests verifying the core behaviors:
public sealed class InsertTextCommandTests
{
[Fact]
public void Execute_EmptyDocument_InsertsText()
{
var document = new Document();
var command = new InsertTextCommand(
document, 0, "Hello");
command.Execute();
Assert.Equal("Hello", document.Content);
}
[Fact]
public void Undo_AfterExecute_RestoresOriginal()
{
var document = new Document("World");
var command = new InsertTextCommand(
document, 0, "Hello ");
command.Execute();
command.Undo();
Assert.Equal("World", document.Content);
}
}
public sealed class DeleteTextCommandTests
{
[Fact]
public void Execute_RemovesSpecifiedRange()
{
var document = new Document("Hello World");
var command = new DeleteTextCommand(
document, 5, 6);
command.Execute();
Assert.Equal("Hello", document.Content);
}
[Fact]
public void Undo_AfterExecute_RestoresDeletedText()
{
var document = new Document("Hello World");
var command = new DeleteTextCommand(
document, 5, 6);
command.Execute();
command.Undo();
Assert.Equal("Hello World", document.Content);
}
}
public sealed class FormatTextCommandTests
{
[Fact]
public void Execute_WrapsBoldMarkers_AroundRange()
{
var document = new Document("Hello World");
var command = new FormatTextCommand(
document, 0, 5, "**", "**");
command.Execute();
Assert.Equal("**Hello** World", document.Content);
}
[Fact]
public void Undo_AfterExecute_RemovesFormatting()
{
var document = new Document("Hello World");
var command = new FormatTextCommand(
document, 0, 5, "**", "**");
command.Execute();
command.Undo();
Assert.Equal("Hello World", document.Content);
}
}
Each test follows a clear pattern: arrange, execute, assert. Undo tests verify the document returns to its original state. These tests don't go through the EditorSession -- they test command logic in isolation.
Now let's test the invoker and macro behaviors:
public sealed class CommandInvokerTests
{
[Fact]
public void Undo_ReversesMostRecentCommand()
{
var document = new Document();
var invoker = new CommandInvoker();
invoker.Execute(new InsertTextCommand(
document, 0, "Hello"));
invoker.Execute(new InsertTextCommand(
document, 5, " World"));
invoker.Undo();
Assert.Equal("Hello", document.Content);
}
[Fact]
public void Redo_ReappliesUndoneCommand()
{
var document = new Document();
var invoker = new CommandInvoker();
invoker.Execute(new InsertTextCommand(
document, 0, "Hello"));
invoker.Undo();
invoker.Redo();
Assert.Equal("Hello", document.Content);
}
[Fact]
public void Execute_AfterUndo_ClearsRedoStack()
{
var document = new Document();
var invoker = new CommandInvoker();
invoker.Execute(new InsertTextCommand(
document, 0, "First"));
invoker.Undo();
invoker.Execute(new InsertTextCommand(
document, 0, "Second"));
Assert.False(invoker.CanRedo);
Assert.Equal("Second", document.Content);
}
}
public sealed class MacroCommandTests
{
[Fact]
public void Execute_RunsAllCommandsInOrder()
{
var document = new Document();
var macro = new MacroCommand(new ICommand[]
{
new InsertTextCommand(document, 0, "Hello"),
new InsertTextCommand(document, 5, " World"),
});
macro.Execute();
Assert.Equal("Hello World", document.Content);
}
[Fact]
public void Undo_ReversesAllCommandsInReverseOrder()
{
var document = new Document();
var macro = new MacroCommand(new ICommand[]
{
new InsertTextCommand(document, 0, "Hello"),
new InsertTextCommand(document, 5, " World"),
});
macro.Execute();
macro.Undo();
Assert.Equal(string.Empty, document.Content);
}
}
Testing a command pattern system is inherently clean. Commands are small, focused objects with predictable inputs and outputs. You don't need mocking frameworks -- the document is a real object, and each command operates on it directly.
Wiring Everything Up with Dependency Injection
The final step is registering the editor components in the DI container. Since commands are created per-operation rather than resolved from the container, the DI setup focuses on the session-level objects:
using Microsoft.Extensions.DependencyInjection;
public static class EditorServiceRegistration
{
public static IServiceCollection AddTextEditor(
this IServiceCollection services)
{
services.AddTransient<Document>();
services.AddTransient<CommandInvoker>();
services.AddTransient<MacroRecorder>();
services.AddTransient<EditorSession>();
return services;
}
}
Each EditorSession gets its own Document, CommandInvoker, and MacroRecorder via transient registration. Commands themselves aren't registered -- they're created inside EditorSession methods because they depend on runtime parameters like cursor position and selected text. This is a common command pattern characteristic: the invoker and receiver come from DI, but commands are instantiated at the call site.
In your Program.cs or startup configuration:
builder.Services.AddTextEditor();
If you wanted to extend this with additional command types, you'd add the new command class implementing ICommand and a corresponding method on EditorSession. No changes to the invoker, no changes to existing commands, no changes to DI registration.
Command Pattern vs. Strategy Pattern
If you've worked with the strategy pattern, you might notice similarities. Both encapsulate behavior behind an interface. The difference is intent and lifecycle. A strategy is selected once and used repeatedly. A command captures a specific operation with specific parameters at a specific point in time. Commands are stored, queued, undone, and replayed. Strategies are swapped.
In practice, you'll often use both together. Similar compositional thinking applies when you look at how the decorator pattern layers additional behavior onto existing objects or how the adapter pattern bridges incompatible interfaces.
Frequently Asked Questions
When should I use the command pattern instead of direct method calls?
Use the command pattern when you need undo/redo, command history, macro recording, deferred execution, or operation queuing. If you just need to call a method and forget about it, direct calls are simpler. The decision point is whether you need to treat operations as first-class objects that can be stored, reversed, or replayed.
How does the command pattern handle operations that can't be undone?
Some operations are inherently irreversible -- sending an email, charging a credit card. For these, you can implement Undo to throw NotSupportedException, or split your interface into ICommand and IUndoableCommand. The invoker can check at runtime whether a command supports undo before allowing it.
Can I serialize commands for persistence or distributed systems?
Yes, and this is one of the command pattern's strongest features. Because each command is a self-contained object with all its parameters, you can serialize it to JSON, store it in a database, or send it over a message queue. This is the foundation of event sourcing -- your command history becomes your system of record.
What's the difference between the command pattern and the memento pattern?
The memento pattern captures the entire state of an object at a point in time. The command pattern captures the delta -- the specific operation that changed state. Mementos are simpler but more expensive in memory. Commands are more efficient but require each operation to know how to reverse itself.
How do I handle command validation in the command pattern?
Validate before executing. You can add a CanExecute method to your command interface that the invoker checks before calling Execute. This keeps validation logic co-located with execution logic inside each command class.
Should I use async commands in C#?
If your commands interact with I/O -- database writes, API calls, file operations -- then yes, use Task Execute() and Task Undo(). For in-memory operations like our text editor, synchronous commands are simpler. Choose based on what your commands actually do.
How does macro recording interact with undo?
In our implementation, playing a macro executes a MacroCommand through the invoker, so the entire macro is a single entry on the undo stack. One undo reverses all the macro's operations, matching user expectations.
Wrapping Up This Command Pattern Real-World Example
This implementation shows the command pattern doing what it does best -- turning operations into objects that can be executed, undone, redone, recorded, and replayed. We started with an editor class that managed undo through expensive snapshots with no path to redo or macros. We ended with a clean architecture where each operation lives in its own command class, an invoker manages the full undo/redo lifecycle, and macros compose naturally.
The command pattern shines in any system where operations need a history. Text editors, drawing applications, database migrations, game engines with replay -- anywhere users expect to undo their work or replay actions. Take this text editor example, swap the Document class for your own domain model, and you've got a production-ready command infrastructure that keeps your operations testable, your undo logic reliable, and your architecture open to new commands without touching existing code.

