BehavioralC#verifiedVerified
Command Pattern in C#
Encapsulates a request as an object, allowing you to parameterize clients, queue or log requests, and support undoable operations.
How to Implement the Command Pattern in C#
1Step 1: Define the command interface
public interface ICommand
{
void Execute();
void Undo();
}2Step 2: Receiver performs the actual work
public class TextEditor
{
public string Content { get; set; } = "";
public void Insert(string text) => Content += text;
public void Delete(int count) =>
Content = Content[..^Math.Min(count, Content.Length)];
}3Step 3: Concrete command
public class InsertTextCommand(TextEditor editor, string text) : ICommand
{
public void Execute() => editor.Insert(text);
public void Undo() => editor.Delete(text.Length);
}4Step 4: Invoker manages command history
public class CommandHistory
{
private readonly Stack<ICommand> _history = new();
public void ExecuteCommand(ICommand command)
{
command.Execute();
_history.Push(command);
}
public void UndoLast()
{
if (_history.TryPop(out var command))
command.Undo();
}
}using Microsoft.Extensions.Logging;
// [step] Define async command interface with metadata
public interface ICommand
{
string Id { get; }
string Description { get; }
Task ExecuteAsync(CancellationToken ct = default);
Task UndoAsync(CancellationToken ct = default);
}
public record CommandRecord(
string Id, string Description, DateTime ExecutedAt,
bool IsUndone = false);
// [step] Concrete command: file operation with rollback
public sealed class WriteFileCommand(
string path, string content,
ILogger<WriteFileCommand> logger) : ICommand
{
private string? _previousContent;
public string Id { get; } = Guid.NewGuid().ToString();
public string Description => $"Write to {Path.GetFileName(path)}";
public async Task ExecuteAsync(CancellationToken ct = default)
{
if (File.Exists(path))
_previousContent = await File.ReadAllTextAsync(path, ct);
await File.WriteAllTextAsync(path, content, ct);
logger.LogInformation("Wrote {Bytes} bytes to {Path}",
content.Length, path);
}
public async Task UndoAsync(CancellationToken ct = default)
{
if (_previousContent is not null)
await File.WriteAllTextAsync(path, _previousContent, ct);
else
File.Delete(path);
logger.LogInformation("Undid write to {Path}", path);
}
}
// [step] Macro command composes multiple commands
public sealed class MacroCommand(
IReadOnlyList<ICommand> commands) : ICommand
{
public string Id { get; } = Guid.NewGuid().ToString();
public string Description =>
$"Macro ({commands.Count} commands)";
public async Task ExecuteAsync(CancellationToken ct = default)
{
foreach (var cmd in commands)
await cmd.ExecuteAsync(ct);
}
public async Task UndoAsync(CancellationToken ct = default)
{
foreach (var cmd in commands.Reverse())
await cmd.UndoAsync(ct);
}
}
// [step] Command manager with history and redo support
public sealed class CommandManager(ILogger<CommandManager> logger)
{
private readonly Stack<ICommand> _undoStack = new();
private readonly Stack<ICommand> _redoStack = new();
private readonly List<CommandRecord> _log = [];
public IReadOnlyList<CommandRecord> History => _log.AsReadOnly();
public async Task ExecuteAsync(
ICommand command, CancellationToken ct = default)
{
await command.ExecuteAsync(ct);
_undoStack.Push(command);
_redoStack.Clear();
_log.Add(new CommandRecord(
command.Id, command.Description, DateTime.UtcNow));
logger.LogDebug("Executed: {Cmd}", command.Description);
}
public async Task UndoAsync(CancellationToken ct = default)
{
if (!_undoStack.TryPop(out var command)) return;
await command.UndoAsync(ct);
_redoStack.Push(command);
_log.Add(new CommandRecord(
command.Id, $"UNDO: {command.Description}",
DateTime.UtcNow, true));
logger.LogDebug("Undid: {Cmd}", command.Description);
}
public async Task RedoAsync(CancellationToken ct = default)
{
if (!_redoStack.TryPop(out var command)) return;
await command.ExecuteAsync(ct);
_undoStack.Push(command);
logger.LogDebug("Redid: {Cmd}", command.Description);
}
}Command Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Command Pattern in the Real World
“Think of a restaurant order ticket. A waiter (invoker) takes your order and writes it on a slip (command). The slip is handed to the kitchen (receiver) which executes it. The waiter doesn't cook anything—they just carry and deliver orders. Tickets can be queued, cancelled before cooking, or reviewed in an audit log at day's end.”