Learnixo

.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

C#
// 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: £1000

Command Queue (Background Processing)

C#
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)

C#
// 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

C#
// 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 — PlaceOrderCommand carries 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."