How to Implement Command Pattern in C#: Step-by-Step Guide
Turning method calls into objects sounds odd at first, but it unlocks powerful capabilities like undo/redo, queuing, and macro recording. The command pattern is a behavioral design pattern that encapsulates a request as an object, letting you parameterize clients with different requests, log operations, and support reversible actions. If you want to implement command pattern in C#, this guide walks you through the entire process from defining the command interface to wiring everything into a dependency injection container. By the end, you'll have complete, working code that covers simple commands, composite macros, and full undo/redo support.
We'll build progressively -- starting with a minimal interface, then adding a receiver, concrete commands, an invoker with history tracking, undo/redo mechanics, and finally DI registration. Each step includes complete C# code you can compile and adapt to your own projects.
Prerequisites
Before getting started, make sure you're familiar with these fundamentals:
- C# interfaces and classes: You'll define a command interface and implement multiple concrete command classes. Understanding how interfaces enforce contracts is essential.
- Composition over inheritance: When you implement command pattern in C#, commands hold references to receiver objects through composition. This keeps commands decoupled from the invoker.
- Dependency injection basics: The final step covers registering commands and invokers with IServiceCollection. Familiarity with service registration will help.
- .NET 8 or later: The code examples use modern C# syntax. Any recent .NET SDK works.
Step 1: Define the ICommand Interface
The first step to implement command pattern in C# is defining the command interface. This interface declares the operations every command must support. At minimum, a command needs an Execute method. Since we're building toward undo/redo support, we'll include an Undo method from the start.
public interface ICommand
{
string Description { get; }
void Execute();
void Undo();
}
The interface is intentionally small. Every concrete command must implement these three members, so keeping the contract narrow means less work per command and a cleaner abstraction. The Description property gives the invoker a human-readable label for logging and history display -- this is optional but extremely useful in practice.
Notice that both Execute and Undo are parameterless. This is a deliberate design choice. When you implement command pattern in C#, all the data a command needs to do its work should be captured at construction time, not passed in at execution time. The command object is the request -- it carries everything with it.
This approach aligns with the principle of inversion of control. The invoker depends on the ICommand abstraction, never on the concrete command classes. That separation lets you add new commands without modifying the invoker.
Step 2: Create the Receiver Class
The receiver is the object that performs the actual work. When you implement command pattern in C#, the receiver contains the business logic that commands invoke. Commands delegate to the receiver rather than doing the work themselves -- this keeps commands lightweight and focused on orchestration.
For our example, we'll build a simple document editor that tracks text content:
public sealed class TextDocument
{
private readonly List<string> _lines = new();
public IReadOnlyList<string> Lines => _lines;
public string Title { get; private set; } = "Untitled";
public void AddLine(string text)
{
_lines.Add(text);
Console.WriteLine(
$"[TextDocument] Added line: "{text}"");
}
public void RemoveLine(int index)
{
if (index < 0 || index >= _lines.Count)
{
Console.WriteLine(
$"[TextDocument] Cannot remove line " +
$"at index {index} -- out of range.");
return;
}
string removed = _lines[index];
_lines.RemoveAt(index);
Console.WriteLine(
$"[TextDocument] Removed line: "{removed}"");
}
public void SetTitle(string title)
{
string previous = Title;
Title = title;
Console.WriteLine(
$"[TextDocument] Title changed from " +
$""{previous}" to "{title}"");
}
public void PrintDocument()
{
Console.WriteLine($"--- {Title} ---");
for (int i = 0; i < _lines.Count; i++)
{
Console.WriteLine($" {i}: {_lines[i]}");
}
Console.WriteLine("--- end ---");
}
}
The TextDocument class knows nothing about commands, invokers, or undo history. It's pure domain logic. This separation matters because you can test the receiver independently and reuse it in contexts that don't involve the command pattern at all.
Step 3: Implement Concrete Commands
Now we build the concrete command classes. Each one implements ICommand, holds a reference to the receiver, and captures the data it needs at construction time. This is the step where you really implement command pattern in C# -- each command encapsulates a single operation and its inverse.
AddLineCommand
public sealed class AddLineCommand : ICommand
{
private readonly TextDocument _document;
private readonly string _text;
public string Description =>
$"Add line: "{_text}"";
public AddLineCommand(
TextDocument document,
string text)
{
_document = document
?? throw new ArgumentNullException(
nameof(document));
_text = text;
}
public void Execute()
{
_document.AddLine(_text);
}
public void Undo()
{
int lastIndex = _document.Lines.Count - 1;
_document.RemoveLine(lastIndex);
}
}
SetTitleCommand
public sealed class SetTitleCommand : ICommand
{
private readonly TextDocument _document;
private readonly string _newTitle;
private string _previousTitle = string.Empty;
public string Description =>
$"Set title: "{_newTitle}"";
public SetTitleCommand(
TextDocument document,
string newTitle)
{
_document = document
?? throw new ArgumentNullException(
nameof(document));
_newTitle = newTitle;
}
public void Execute()
{
_previousTitle = _document.Title;
_document.SetTitle(_newTitle);
}
public void Undo()
{
_document.SetTitle(_previousTitle);
}
}
A few things to notice about these commands:
- Each command captures all the data it needs through its constructor. No parameters flow through
ExecuteorUndo. SetTitleCommandstores the previous title in_previousTitleduringExecuteso it can restore it duringUndo. This snapshot-on-execute approach is the standard technique when you implement command pattern with undo support.- Both commands delegate all real work to the
TextDocumentreceiver. The commands themselves contain no business logic -- just orchestration.
This structure maps closely to how the strategy design pattern encapsulates algorithms behind interfaces. The key difference is intent: strategies are interchangeable algorithms, while commands are encapsulated requests that can be stored, queued, and reversed.
Step 4: Build the Invoker with Command History
The invoker is the object that triggers command execution and manages the command lifecycle. When you implement command pattern in C#, the invoker is where command history lives. It tracks executed commands so it can support undo and redo later.
public sealed class CommandInvoker
{
private readonly Stack<ICommand> _undoStack = new();
private readonly Stack<ICommand> _redoStack = new();
public IReadOnlyCollection<ICommand> UndoHistory =>
_undoStack;
public IReadOnlyCollection<ICommand> RedoHistory =>
_redoStack;
public void ExecuteCommand(ICommand command)
{
if (command is null)
{
throw new ArgumentNullException(
nameof(command));
}
command.Execute();
_undoStack.Push(command);
_redoStack.Clear();
Console.WriteLine(
$"[Invoker] Executed: {command.Description}");
}
public void PrintHistory()
{
Console.WriteLine("[Invoker] Command history:");
int index = 0;
foreach (ICommand cmd in _undoStack)
{
Console.WriteLine(
$" {index}: {cmd.Description}");
index++;
}
}
}
The invoker works with ICommand exclusively -- it never references AddLineCommand, SetTitleCommand, or TextDocument. This means you can add entirely new command types without touching the invoker. That's a direct benefit of choosing to implement command pattern in C# with a clean abstraction layer.
Notice that ExecuteCommand clears the redo stack whenever a new command runs. This is standard behavior -- if you undo three actions and then perform a new one, the undone actions are discarded. This prevents confusing branching histories.
Step 5: Add Undo/Redo Support
With the history stacks already in place, adding undo and redo to implement command pattern with full reversibility is straightforward. Each undo pops from the undo stack, calls Undo, and pushes to the redo stack. Redo does the reverse.
Add these methods to the CommandInvoker class:
public void Undo()
{
if (_undoStack.Count == 0)
{
Console.WriteLine(
"[Invoker] Nothing to undo.");
return;
}
ICommand command = _undoStack.Pop();
command.Undo();
_redoStack.Push(command);
Console.WriteLine(
$"[Invoker] Undid: {command.Description}");
}
public void Redo()
{
if (_redoStack.Count == 0)
{
Console.WriteLine(
"[Invoker] Nothing to redo.");
return;
}
ICommand command = _redoStack.Pop();
command.Execute();
_undoStack.Push(command);
Console.WriteLine(
$"[Invoker] Redid: {command.Description}");
}
Here's a complete example showing the undo/redo workflow in action:
var document = new TextDocument();
var invoker = new CommandInvoker();
invoker.ExecuteCommand(
new SetTitleCommand(document, "Meeting Notes"));
invoker.ExecuteCommand(
new AddLineCommand(document, "Discuss Q3 roadmap"));
invoker.ExecuteCommand(
new AddLineCommand(document, "Review budget"));
document.PrintDocument();
// --- Meeting Notes ---
// 0: Discuss Q3 roadmap
// 1: Review budget
// --- end ---
invoker.Undo(); // Removes "Review budget"
invoker.Undo(); // Removes "Discuss Q3 roadmap"
document.PrintDocument();
// --- Meeting Notes ---
// --- end ---
invoker.Redo(); // Re-adds "Discuss Q3 roadmap"
document.PrintDocument();
// --- Meeting Notes ---
// 0: Discuss Q3 roadmap
// --- end ---
The undo/redo mechanism works because each command captures enough state to reverse itself. AddLineCommand knows it added the last line, so it removes the last line. SetTitleCommand snapshots the previous title, so it can restore it. When you implement command pattern with proper undo support, this approach scales to any operation -- as long as each command can reverse what it did, the invoker handles the rest.
Composite Commands: Macro Recording
Sometimes a single user action should trigger multiple commands in sequence. This is where composite commands -- also called macro commands -- come in. A macro command implements ICommand and contains a list of child commands. When executed, it runs all children in order. When undone, it reverses them.
This approach borrows directly from the composite design pattern, which structures objects into tree hierarchies so individual objects and groups share the same interface. Here, the "tree" is a flat list of commands, but the principle is identical -- the macro implements ICommand just like any leaf command, so the invoker treats it the same way.
public sealed class MacroCommand : ICommand
{
private readonly List<ICommand> _commands;
public string Description { get; }
public MacroCommand(
string description,
IEnumerable<ICommand> commands)
{
Description = description;
_commands = commands?.ToList()
?? throw new ArgumentNullException(
nameof(commands));
}
public void Execute()
{
foreach (ICommand command in _commands)
{
command.Execute();
}
}
public void Undo()
{
for (int i = _commands.Count - 1; i >= 0; i--)
{
_commands[i].Undo();
}
}
}
Notice that Undo iterates in reverse order. This is critical -- if three commands ran in sequence, undoing them must happen in the opposite order to maintain consistency. The macro captures the child list at construction time so it's immutable after creation.
Here's how to use the macro with the invoker:
var document = new TextDocument();
var invoker = new CommandInvoker();
var setupMacro = new MacroCommand(
"Initial document setup",
new ICommand[]
{
new SetTitleCommand(document, "Sprint Retro"),
new AddLineCommand(document, "What went well"),
new AddLineCommand(document, "What to improve"),
new AddLineCommand(document, "Action items"),
});
invoker.ExecuteCommand(setupMacro);
document.PrintDocument();
// --- Sprint Retro ---
// 0: What went well
// 1: What to improve
// 2: Action items
// --- end ---
invoker.Undo(); // Undoes the entire macro
document.PrintDocument();
// --- Untitled ---
// --- end ---
A single undo reverses all four operations because the invoker sees the macro as one command. This is exactly the power of implementing the command pattern with composites -- complex operations become first-class objects that slot into the same infrastructure.
Step 6: Wire Up with Dependency Injection
For production applications, you'll want to register your receivers and invoker with a DI container. When you implement command pattern in C#, the invoker and receivers are typically long-lived services, while individual commands are created on demand through factory methods or delegates.
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<TextDocument>();
services.AddSingleton<CommandInvoker>();
var provider = services.BuildServiceProvider();
var document = provider
.GetRequiredService<TextDocument>();
var invoker = provider
.GetRequiredService<CommandInvoker>();
invoker.ExecuteCommand(
new AddLineCommand(document, "First line via DI"));
invoker.ExecuteCommand(
new SetTitleCommand(document, "DI-Managed Doc"));
document.PrintDocument();
Commands themselves are not registered in the container because they're short-lived, parameterized objects. Each command captures specific data -- a line of text, a new title -- that varies per invocation. This is a key distinction when you implement command pattern in DI-managed applications. The consuming code creates commands using the resolved receiver and passes them to the resolved invoker.
If you find that command creation logic gets complex or repetitive, extract a factory class:
public sealed class DocumentCommandFactory
{
private readonly TextDocument _document;
public DocumentCommandFactory(TextDocument document)
{
_document = document
?? throw new ArgumentNullException(
nameof(document));
}
public ICommand CreateAddLine(string text)
{
return new AddLineCommand(_document, text);
}
public ICommand CreateSetTitle(string title)
{
return new SetTitleCommand(_document, title);
}
public ICommand CreateMacro(
string description,
params ICommand[] commands)
{
return new MacroCommand(description, commands);
}
}
Register the factory in the container and inject it wherever commands need to be created:
services.AddSingleton<DocumentCommandFactory>();
This factory approach keeps command construction centralized. The consuming class asks the factory for commands and passes them to the invoker -- it never needs to know about AddLineCommand or SetTitleCommand directly. That level of decoupling makes your system easier to extend. Adding a new command type means updating the factory and the receiver -- the invoker and all client code stay untouched.
This layered approach of abstractions and factories follows the same principles behind the decorator pattern, where behavior is composed through wrapping rather than modification. You can even combine the two -- wrap commands with a logging decorator that records every execution before delegating to the real command.
Common Mistakes to Avoid
Even experienced developers make these mistakes when they first implement command pattern in C#.
Putting business logic inside the command: Commands should delegate to the receiver, not duplicate its logic. If you find your command class growing complex, the work probably belongs in the receiver. When you implement command pattern correctly, commands orchestrate -- they don't compute.
Forgetting to capture undo state during Execute: The SetTitleCommand example captures _previousTitle at execution time. If you forget this step, Undo has no state to restore. Always snapshot whatever you need to reverse the operation before making changes.
Sharing mutable state between commands: Each command should be self-contained. If two commands share a mutable reference, undoing one can break the other. When you implement command pattern in C#, treat each command instance as immutable after construction -- except for the undo state it captures during Execute.
Not clearing the redo stack on new executions: If a user undoes two actions, then performs a new one, the redo stack must be cleared. Otherwise, redoing would replay actions that no longer make sense in the current context.
Creating commands that can't be undone: If you include Undo in your interface, every command must implement it meaningfully. An Undo method that does nothing silently is worse than throwing NotSupportedException -- at least the exception makes the limitation visible. If some commands genuinely can't be reversed, consider splitting your interface into ICommand and IUndoableCommand.
Frequently Asked Questions
What is the command pattern and when should I use it in C#?
The command pattern is a behavioral design pattern that turns a request into a standalone object containing all the information needed to perform the action. You should implement command pattern in C# when you need undo/redo functionality, operation queuing, macro recording, or when you want to decouple the object that initiates an operation from the object that performs it. It's especially valuable in editor-style applications, workflow engines, and any system that needs an operation log.
How does the command pattern differ from the strategy pattern?
Both patterns encapsulate behavior behind interfaces, but their intent differs. The strategy pattern lets you swap algorithms at runtime -- the caller chooses which strategy to use and invokes it immediately. When you implement command pattern in C#, you encapsulate a request as an object that can be stored, queued, logged, or undone. Strategies are about how to do something. Commands are about what to do -- and when to do it.
Can I implement async commands in C#?
Yes. Define an IAsyncCommand interface with Task ExecuteAsync() and Task UndoAsync() methods. Your invoker's ExecuteCommand method becomes async as well. The core pattern stays the same -- you're just swapping synchronous calls for asynchronous ones. Be careful with undo state capture in async scenarios, though. Make sure the state snapshot happens before any await to avoid race conditions.
How do I handle commands that cannot be undone?
There are two clean approaches. First, you can split your interface into ICommand (execute only) and IUndoableCommand (execute plus undo). The invoker only pushes IUndoableCommand instances onto the undo stack. Second, you can keep a single interface and have non-reversible commands throw NotSupportedException from Undo, with the invoker checking a CanUndo property before attempting it. The right choice depends on how many non-undoable commands you have.
How does the command pattern support macro recording?
Macro recording uses composite commands. A MacroCommand implements ICommand and holds a list of child commands. When executed, it runs all children in order. When undone, it reverses them. You start recording by collecting commands into a list instead of executing them immediately. When the user stops recording, you wrap the list in a MacroCommand. The invoker treats the macro exactly like any other command -- one execute, one undo entry -- even though it contains multiple operations internally.
What is the relationship between the command pattern and dependency injection?
When you implement command pattern in C#, DI handles the long-lived components -- the receiver and the invoker. Commands themselves are short-lived, parameterized objects created on demand. You typically register a command factory in the container rather than individual command classes. The factory receives the receiver through constructor injection and provides methods to create specific commands. This follows inversion of control naturally -- consuming code depends on the factory abstraction, not concrete command types.
Can I combine the command pattern with other design patterns?
Absolutely. The command pattern combines well with several other patterns. Use the decorator pattern to wrap commands with logging, validation, or retry logic without modifying the commands themselves. Use the composite pattern for macro commands, as shown earlier in this guide. And you can use the adapter pattern to wrap legacy operations behind the ICommand interface so they integrate into your command infrastructure seamlessly.

