.NET & C# Development · Lesson 44 of 229
Command — Encapsulate Requests as Objects
Command — Encapsulate Requests as Objects
The Command pattern encapsulates a request as an object, allowing you to parameterise clients with different requests, queue or log requests, and support undo/redo operations.
Core Structure
// Command interface
public interface ICommand
{
Task ExecuteAsync();
Task UndoAsync();
}
// Receiver — the object that knows how to perform the work
public class BankAccount
{
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
Balance += amount;
Console.WriteLine($"Deposited £{amount:F2} — Balance: £{Balance:F2}");
}
public void Withdraw(decimal amount)
{
if (amount > Balance) throw new InvalidOperationException("Insufficient funds");
Balance -= amount;
Console.WriteLine($"Withdrew £{amount:F2} — Balance: £{Balance:F2}");
}
}
// Concrete Commands
public class DepositCommand(BankAccount account, decimal amount) : ICommand
{
public Task ExecuteAsync() { account.Deposit(amount); return Task.CompletedTask; }
public Task UndoAsync() { account.Withdraw(amount); return Task.CompletedTask; }
}
public class WithdrawCommand(BankAccount account, decimal amount) : ICommand
{
public Task ExecuteAsync() { account.Withdraw(amount); return Task.CompletedTask; }
public Task UndoAsync() { account.Deposit(amount); return Task.CompletedTask; }
}
// Invoker — executes commands and maintains history for undo
public class TransactionManager
{
private readonly Stack<ICommand> _history = new();
public async Task ExecuteAsync(ICommand command)
{
await command.ExecuteAsync();
_history.Push(command);
}
public async Task UndoAsync()
{
if (_history.TryPop(out var command))
await command.UndoAsync();
}
}
// Usage
var account = new BankAccount();
var manager = new TransactionManager();
await manager.ExecuteAsync(new DepositCommand(account, 1000)); // Balance: £1000
await manager.ExecuteAsync(new WithdrawCommand(account, 250)); // Balance: £750
await manager.UndoAsync(); // Balance: £1000Command Queue (Background Processing)
public class CommandQueue
{
private readonly Channel<ICommand> _channel =
Channel.CreateUnbounded<ICommand>();
public async Task EnqueueAsync(ICommand command)
=> await _channel.Writer.WriteAsync(command);
public async Task ProcessAsync(CancellationToken ct)
{
await foreach (var command in _channel.Reader.ReadAllAsync(ct))
{
try
{
await command.ExecuteAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Command failed: {ex.Message}");
}
}
}
}
// Enqueue from one thread, process from another
var queue = new CommandQueue();
await queue.EnqueueAsync(new DepositCommand(account, 500));Macro Command (Composite Command)
// Execute multiple commands as one atomic unit
public class MacroCommand(IEnumerable<ICommand> commands) : ICommand
{
private readonly List<ICommand> _commands = commands.ToList();
private readonly Stack<ICommand> _executed = new();
public async Task ExecuteAsync()
{
foreach (var cmd in _commands)
{
await cmd.ExecuteAsync();
_executed.Push(cmd);
}
}
public async Task UndoAsync()
{
while (_executed.TryPop(out var cmd))
await cmd.UndoAsync(); // undo in reverse order
}
}CQRS Commands = Command Pattern
// In CQRS, every command is a Command pattern object
// The handler is the Receiver, the Mediator is the Invoker
public record PlaceOrderCommand(int CustomerId, List<OrderItemDto> Items)
: IRequest<int>;
public class PlaceOrderHandler(IOrderRepository repo, IEventBus events)
: IRequestHandler<PlaceOrderCommand, int>
{
public async Task<int> Handle(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.CustomerId, cmd.Items);
await repo.AddAsync(order, ct);
await events.PublishAsync(new OrderPlacedEvent(order.Id), ct);
return order.Id;
}
}When to Use
✓ Undo/redo support (document editors, drawing apps)
✓ Request queuing, scheduling, or delayed execution
✓ Logging all operations (audit trail, replay)
✓ Transaction-like behaviour with rollback
✓ CQRS — commands as data objects
✗ Simple one-off calls with no need for history/queuing
✗ When undo semantics are hard to define (e.g., sending an email)Interview Answer
"The Command pattern encapsulates a request (operation + parameters) as an object, decoupling the sender from the receiver and enabling queuing, logging, and undo. The key players: Command (interface with Execute/Undo), ConcreteCommand (holds a reference to the receiver and implements the operation), Receiver (BankAccount, document, etc.), and Invoker (executes commands, maintains history). In modern C# applications, CQRS commands are a direct application —
PlaceOrderCommandcarries the intent as data, the handler is the receiver, and the mediator is the invoker. Undo/redo is the classic use case: push commands onto a stack on execute, pop and call Undo on undo. Command queues separate producers from consumers — useful for rate limiting or background processing."