Mastering Design Patterns in C# and ASP.NET Core
Part 4.2 – Command Design Pattern
Series: Design Patterns in C# and ASP.NET Core
Pattern Category: Behavioral Design Pattern
Difficulty: ⭐⭐⭐⭐☆ Intermediate to Advanced
Prerequisites: C#, OOP, SOLID Principles, Interfaces, Dependency Injection, ASP.NET Core
Table of Contents
Introduction
What is the Command Design Pattern?
Why Do We Need the Command Pattern?
Command Pattern Terminology
UML Class Diagram
How the Command Pattern Works
Complete C# Console Application
Multiple Commands Example
ASP.NET Core Implementation
Command and Handler Architecture
Command Pattern and CQRS
Undo/Redo Functionality
Queue and Background Processing
Banking Transaction Example
E-Commerce Order Example
Real-World Enterprise Scenarios
Advantages
Disadvantages
Best Practices
Common Mistakes
Command vs Strategy
Command vs Mediator
Command vs Chain of Responsibility
Interview Questions
Summary
Coming Up Next
1. Introduction
In traditional application development, we often call a method directly:
orderService.CreateOrder();
This approach is perfectly fine for simple applications.
However, enterprise applications frequently need more than simply executing a method.
For example, an operation may need to be:
Logged
Validated
Queued
Stored
Retried
Executed asynchronously
Undone
Audited
Sent to another service
Processed through CQRS
This is where the Command Design Pattern becomes useful.
Instead of directly saying:
"Execute this method."
we create an object representing:
"Do this operation."
That object is called a Command.
2. What is the Command Design Pattern?
Definition
The Command Design Pattern is a behavioral design pattern that encapsulates a request or operation as an object.
In simple terms:
The Command Pattern turns a request into a standalone object.
This allows us to:
Pass requests around
Store requests
Queue requests
Log requests
Retry requests
Undo requests
Execute requests later
Instead of:
service.CreateOrder(order);
we can create:
CreateOrderCommand
and execute it through a handler.
3. Why Do We Need the Command Pattern?
Consider an e-commerce application.
Without Command Pattern:
public void CreateOrder(Order order)
{
ValidateOrder(order);
CheckInventory(order);
ProcessPayment(order);
SaveOrder(order);
SendEmail(order);
PublishEvent(order);
}
The operation is tightly coupled to the implementation.
Now imagine that tomorrow we need to:
Execute it asynchronously
Add retry logic
Put it on a queue
Log it
Undo it
Schedule it
Send it to another service
The design becomes more complicated.
With Command:
CreateOrderCommand
↓
Command Handler
↓
Order Service
↓
Database
The request itself becomes a first-class object.
4. Command Pattern Terminology
There are four major participants.
1. Command
Represents the operation/request.
Example:
CreateOrderCommand
UpdateCustomerCommand
CancelOrderCommand
ProcessPaymentCommand
2. Receiver
The actual object that knows how to perform the operation.
Example:
OrderService
PaymentService
CustomerService
3. Invoker
The object that tells the command to execute.
It doesn't need to know how the operation is implemented.
Example:
CommandInvoker
Queue
Controller
UI Button
Background Worker
4. Client
Creates the command and configures the necessary objects.
Example:
Controller
Application Service
UI
5. UML Class Diagram
The traditional Command Pattern can be represented as:
+----------------+
| Client |
+-------+--------+
|
| creates
↓
+----------------+
| Command |
+----------------+
| + Execute() |
+-------^--------+
|
+--------------+--------------+
| |
| |
+--------------------------+ +--------------------------+
| ConcreteCommand | | ConcreteCommand |
+--------------------------+ +--------------------------+
| - receiver | | - receiver |
| + Execute() | | + Execute() |
+------------+-------------+ +--------------------------+
|
| calls
↓
+-------------+
| Receiver |
+-------------+
| + Action() |
+-------------+
↑
|
+-------------+
| Invoker |
+-------------+
| + Execute() |
+-------------+
6. How the Command Pattern Works
The basic flow is:
Client
↓
Create Command
↓
Pass Command to Invoker
↓
Invoker calls Execute()
↓
Concrete Command
↓
Receiver
↓
Business Operation
For example:
Customer
↓
CreateOrderCommand
↓
Command Handler
↓
OrderService
↓
Database
The important point is:
The invoker does not need to know the internal details of the operation.
7. Complete C# Console Application
Let's create a simple banking example.
We want to support:
Deposit
Withdraw
Step 1 – Command Interface
public interface ICommand
{
void Execute();
}
Step 2 – Receiver
The receiver performs the actual business operation.
public class BankAccount
{
private decimal _balance;
public BankAccount(decimal initialBalance)
{
_balance = initialBalance;
}
public void Deposit(decimal amount)
{
_balance += amount;
Console.WriteLine(
$"Deposited ${amount:N2}");
Console.WriteLine(
$"Current Balance: ${_balance:N2}");
}
public void Withdraw(decimal amount)
{
if (amount > _balance)
{
Console.WriteLine(
"Insufficient balance.");
return;
}
_balance -= amount;
Console.WriteLine(
$"Withdrawn ${amount:N2}");
Console.WriteLine(
$"Current Balance: ${_balance:N2}");
}
}
Step 3 – Deposit Command
public class DepositCommand : ICommand
{
private readonly BankAccount _account;
private readonly decimal _amount;
public DepositCommand(
BankAccount account,
decimal amount)
{
_account = account;
_amount = amount;
}
public void Execute()
{
_account.Deposit(_amount);
}
}
Step 4 – Withdraw Command
public class WithdrawCommand : ICommand
{
private readonly BankAccount _account;
private readonly decimal _amount;
public WithdrawCommand(
BankAccount account,
decimal amount)
{
_account = account;
_amount = amount;
}
public void Execute()
{
_account.Withdraw(_amount);
}
}
Step 5 – Invoker
public class CommandInvoker
{
public void ExecuteCommand(ICommand command)
{
command.Execute();
}
}
Step 6 – Client
class Program
{
static void Main()
{
var account = new BankAccount(5000);
var depositCommand =
new DepositCommand(account, 1000);
var withdrawCommand =
new WithdrawCommand(account, 500);
var invoker = new CommandInvoker();
invoker.ExecuteCommand(depositCommand);
invoker.ExecuteCommand(withdrawCommand);
}
}
Output
Deposited $1,000.00
Current Balance: $6,000.00
Withdrawn $500.00
Current Balance: $5,500.00
Notice the separation:
Command
↓
Invoker
↓
Receiver
8. Multiple Commands Example
A real application can have many commands:
Order Commands
│
├── CreateOrderCommand
├── UpdateOrderCommand
├── CancelOrderCommand
├── ShipOrderCommand
└── CompleteOrderCommand
For example:
public interface ICommand
{
void Execute();
}
Then:
public class CreateOrderCommand : ICommand
{
public void Execute()
{
Console.WriteLine("Creating order...");
}
}
And:
public class CancelOrderCommand : ICommand
{
public void Execute()
{
Console.WriteLine("Cancelling order...");
}
}
The invoker doesn't need separate logic for every command:
public void ExecuteCommand(ICommand command)
{
command.Execute();
}
This is one of the important benefits of the pattern.
9. ASP.NET Core Implementation
Now let's convert the concept into a modern ASP.NET Core application.
Imagine an API endpoint:
POST /api/orders
The controller receives an HTTP request.
Instead of putting all business logic inside the controller:
Controller
↓
Business Logic
↓
Database
we can use:
Controller
↓
CreateOrderCommand
↓
Command Handler
↓
Order Service
↓
Repository
↓
Database
10. Command and Handler Architecture
A simple command could be:
public record CreateOrderCommand(
int CustomerId,
decimal TotalAmount);
The handler:
public class CreateOrderCommandHandler
{
public async Task<int> Handle(
CreateOrderCommand command)
{
Console.WriteLine(
$"Creating order for customer {command.CustomerId}");
// Business logic
// Save to database
await Task.CompletedTask;
return 1001;
}
}
Controller:
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly CreateOrderCommandHandler _handler;
public OrdersController(
CreateOrderCommandHandler handler)
{
_handler = handler;
}
[HttpPost]
public async Task<IActionResult> Create(
CreateOrderCommand command)
{
var orderId =
await _handler.Handle(command);
return Ok(new
{
OrderId = orderId
});
}
}
Register it:
builder.Services.AddScoped<
CreateOrderCommandHandler>();
This approach keeps controllers thin and moves business operations into handlers.
11. Command Pattern and CQRS
This is one of the most important areas for .NET developers.
CQRS stands for:
Command Query Responsibility Segregation
CQRS separates:
Commands
↓
Change State
Queries
↓
Read State
Example:
Application
|
+--------+--------+
| |
Command Query
| |
↓ ↓
Write Handler Query Handler
| |
↓ ↓
Write Database Read Database
Commands might include:
CreateOrderCommand
UpdateOrderCommand
CancelOrderCommand
ApproveLoanCommand
ProcessPaymentCommand
Queries might include:
GetOrderByIdQuery
GetCustomerQuery
GetOrdersQuery
The Command Pattern provides a natural way to represent the write-side operations in CQRS.
12. Command Pattern with CQRS
A typical architecture:
Angular Application
↓
ASP.NET Core Controller
↓
Command
↓
Mediator
↓
Command Handler
↓
Domain Service
↓
Repository
↓
Database
For example:
POST /orders
↓
CreateOrderCommand
↓
CreateOrderCommandHandler
↓
OrderService
↓
OrderRepository
↓
SQL Server
This is very common in modern .NET enterprise applications.
13. Undo/Redo Functionality
One of the classic applications of Command is Undo/Redo.
Suppose a text editor supports:
Type
Delete
Copy
Paste
Each operation can be represented as a command.
TypeCommand
DeleteCommand
CopyCommand
PasteCommand
The application can store commands:
Command History
1. TypeCommand
2. TypeCommand
3. DeleteCommand
4. PasteCommand
Then Undo can reverse the most recent operation.
Example
public interface ICommand
{
void Execute();
void Undo();
}
Example:
public class AddTextCommand : ICommand
{
private readonly TextEditor _editor;
private readonly string _text;
public AddTextCommand(
TextEditor editor,
string text)
{
_editor = editor;
_text = text;
}
public void Execute()
{
_editor.AddText(_text);
}
public void Undo()
{
_editor.RemoveText(_text);
}
}
Now commands can be stored in a history collection.
Stack<ICommand> history = new();
After executing:
command.Execute();
history.Push(command);
Undo:
var command = history.Pop();
command.Undo();
This is one of the classic reasons the Command Pattern is useful.
14. Queue and Background Processing
Commands can also be placed into queues.
Instead of:
HTTP Request
↓
Process Immediately
we can use:
HTTP Request
↓
Create Command
↓
Queue
↓
Background Worker
↓
Command Handler
↓
Business Logic
This is useful for long-running operations.
Examples:
Sending emails
Generating reports
Processing files
Image processing
Notifications
Batch processing
Order processing
Example Concept
Queue<ICommand> commandQueue = new();
commandQueue.Enqueue(
new CreateOrderCommand(...));
A background worker can later process commands from the queue.
In production applications, you would typically use an appropriate durable messaging or background-processing mechanism rather than an in-memory Queue<T>.
15. Banking Transaction Example
Consider a banking system.
Operations may include:
Deposit
Withdraw
Transfer
PayBill
ProcessLoanPayment
Each can become a command:
DepositCommand
WithdrawCommand
TransferCommand
PayBillCommand
LoanPaymentCommand
Architecture:
Banking API
↓
Command
↓
Command Handler
↓
Banking Service
↓
Repository
↓
Database
For example:
POST /api/accounts/transfer
↓
TransferMoneyCommand
↓
TransferMoneyCommandHandler
↓
AccountService
↓
Debit Account
↓
Credit Account
↓
Transaction Record
This makes the transaction operation explicit and easier to test and audit.
16. E-Commerce Order Example
An e-commerce system might have:
CreateOrderCommand
UpdateOrderCommand
CancelOrderCommand
PayOrderCommand
ShipOrderCommand
DeliverOrderCommand
The architecture could look like:
Angular
↓
ASP.NET Core API
↓
Command
↓
Command Handler
↓
Domain Service
↓
Repository
↓
Database
For example:
CreateOrderCommand
↓
CreateOrderCommandHandler
↓
Validate Order
↓
Check Inventory
↓
Calculate Total
↓
Save Order
↓
Publish Event
This keeps the command handler focused on coordinating the use case.
17. Real-World Enterprise Scenarios
The Command Pattern can be used in:
Banking
Money transfers
Deposits
Withdrawals
Loan processing
Bill payments
E-Commerce
Create order
Cancel order
Ship order
Process payment
Refund payment
Customer Management
Create customer
Update customer
Delete customer
Change customer status
Background Processing
Generate report
Send email
Process document
Process batch
Distributed Systems
Message processing
Event-driven operations
Asynchronous commands
Retryable operations
CQRS
Commands represent state-changing operations.
18. Advantages
1. Decouples Sender and Receiver
The invoker doesn't need to know how the operation is implemented.
2. Supports Undo/Redo
Commands can store enough information to reverse an operation.
3. Supports Queuing
Commands can be stored and executed later.
4. Supports Logging
Because the operation is represented as an object, it can be logged.
CreateOrderCommand
CustomerId = 100
Amount = $250
5. Supports Retry
Commands can potentially be retried when processing fails.
6. Good for CQRS
Commands map naturally to state-changing operations.
7. Easier Testing
Command handlers can be tested independently.
19. Disadvantages
1. More Classes
A simple method:
CreateOrder();
could become:
CreateOrderCommand
CreateOrderCommandHandler
CreateOrderValidator
For small applications, this may be unnecessary complexity.
2. More Abstractions
The execution path becomes:
Controller
↓
Command
↓
Handler
↓
Service
↓
Repository
instead of directly calling a service.
3. Potential Boilerplate
Simple CRUD operations may not require a command abstraction.
4. Debugging Can Require More Navigation
Developers may need to follow:
Controller
→ Command
→ Handler
→ Service
→ Repository
20. Best Practices
1. Keep Commands Simple
A command should primarily contain the data required to perform an operation.
Example:
public record CreateCustomerCommand(
string Name,
string Email);
2. Keep Business Logic Out of Controllers
Prefer:
Controller
↓
Command
↓
Handler
rather than putting business logic directly in the controller.
3. One Command Should Represent One Business Operation
Good:
CreateOrderCommand
Avoid overly broad commands such as:
ProcessEverythingCommand
4. Keep Handlers Focused
A handler should coordinate a specific use case rather than becoming a giant business service.
5. Use Dependency Injection
Dependencies should be injected into handlers.
public class CreateOrderCommandHandler
{
private readonly IOrderRepository _repository;
public CreateOrderCommandHandler(
IOrderRepository repository)
{
_repository = repository;
}
}
6. Make Commands Immutable Where Practical
Using C# records is often convenient:
public record CreateOrderCommand(
int CustomerId,
decimal Amount);
This reduces accidental modification after creation.
21. Common Mistakes
Mistake 1 – Putting Business Logic Inside the Command
Avoid:
public class CreateOrderCommand
{
public void Execute()
{
// 500 lines of business logic
}
}
Prefer:
Command
↓
Handler
↓
Business Logic
Mistake 2 – Creating Commands for Every Tiny Method
Not every method needs a command.
Use the pattern when the abstraction provides real value.
Mistake 3 – Creating Giant Handlers
Avoid:
OrderCommandHandler
↓
Create
Update
Delete
Cancel
Ship
Refund
Return
Consider separate commands for separate business operations.
Mistake 4 – Mixing Queries and Commands
In a CQRS-oriented architecture, keep read and write responsibilities clear.
Mistake 5 – Using Commands Without a Reason
Don't use Command merely because it is a design pattern.
Ask:
Does representing this operation as an object provide a meaningful architectural benefit?
22. Command vs Strategy
These patterns can look similar because both use interfaces and implementations.
But their intent is different.
| Feature | Command | Strategy |
|---|---|---|
| Primary Purpose | Encapsulate a request | Encapsulate an algorithm |
| Focus | What should be done? | How should it be done? |
| Can be Queued? | Yes | Usually not the main goal |
| Undo/Redo | Common use case | Not typical |
| CQRS | Common | Not primary |
| Algorithm Selection | Not primary | Core purpose |
Example
Command:
CreateOrderCommand
Strategy:
IPaymentStrategy
├── CreditCardStrategy
├── BankTransferStrategy
└── WalletStrategy
Think:
Command = What operation should happen?
Strategy = Which algorithm should perform it?
23. Command vs Mediator
Command and Mediator are often used together.
Command
Represents the request:
CreateOrderCommand
Mediator
Routes the command to its handler:
CreateOrderCommand
↓
Mediator
↓
CreateOrderCommandHandler
So:
Command = Request
Mediator = Communication/Dispatch Mechanism
In modern .NET applications, a mediator-style architecture can help avoid controllers directly depending on many application services.
24. Command vs Chain of Responsibility
Both are behavioral patterns, but they solve different problems.
| Feature | Command | Chain of Responsibility |
|---|---|---|
| Main Goal | Encapsulate a request | Pass request through handlers |
| Primary Focus | Represent an operation | Process through a sequence |
| Queue | Very common | Not the primary purpose |
| Undo/Redo | Common | Not typical |
| Handler Chain | Not required | Core concept |
| CQRS | Common | Not primary |
Example:
Command:
CreateOrderCommand
↓
CreateOrderHandler
Chain:
Request
↓
Validation
↓
Authentication
↓
Authorization
↓
Processing
25. Interview Questions
Beginner Level
1. What is the Command Design Pattern?
The Command Pattern encapsulates a request as an object, allowing the request to be passed, stored, queued, logged, and executed independently from the sender.
2. What are the main components?
The traditional components are:
Client
Command
Concrete Command
Receiver
Invoker
3. What is a Receiver?
The Receiver contains the actual business logic that performs the requested operation.
4. What is an Invoker?
The Invoker triggers the command without needing to know how the operation is performed.
5. Why is Command a behavioral pattern?
Because it focuses on encapsulating and controlling behavior or operations rather than object creation or structure.
Intermediate Level
6. What are the real-world applications?
Common examples include:
CQRS
Undo/Redo
Queues
Background processing
Transaction processing
Auditing
Logging
Retryable operations
7. How does Command support CQRS?
Commands represent operations that change application state, while queries retrieve data without changing state.
8. What is the difference between Command and Strategy?
Command represents an operation/request.
Strategy represents an interchangeable algorithm.
9. What is the difference between Command and Mediator?
Command represents the request.
Mediator provides a mechanism for dispatching or coordinating communication between the sender and handler.
10. Can Commands be asynchronous?
Absolutely.
For ASP.NET Core applications, a command handler commonly exposes:
Task<TResult>
For example:
public async Task<int> Handle(
CreateOrderCommand command)
{
// Async database operation
return 1001;
}
Advanced / Architect Level
11. Why are Commands useful in distributed systems?
Commands can represent business operations that can be:
Serialized
Placed on queues
Processed asynchronously
Retried
Logged
Audited
This makes them useful in event-driven and message-driven architectures.
12. Should every API endpoint use Command Pattern?
No.
The pattern should be introduced when it provides meaningful benefits such as:
Complex use cases
CQRS
Asynchronous processing
Command routing
Auditing
Undo/Redo
Message processing
For simple applications, direct service calls may be more appropriate.
13. What is the relationship between Command and Command Handler?
The command contains the request data.
The handler contains the application logic required to execute that request.
Command
↓
"Create this order"
Handler
↓
"Here is how we create the order"
14. How can Commands support idempotency?
In distributed systems, a command can contain a unique request or idempotency identifier.
For example:
public record CreatePaymentCommand(
Guid RequestId,
int AccountId,
decimal Amount);
The handler can check whether the RequestId has already been processed before performing the operation again.
This is particularly important when commands are retried.
15. How can Command Pattern be used with Azure Service Bus or RabbitMQ?
A command can be serialized and published to a messaging system:
API
↓
Command
↓
Message Broker
↓
Consumer
↓
Command Handler
↓
Business Logic
This enables asynchronous processing and decouples the producer from the consumer.
26. Command Pattern in a Modern .NET Architecture
A practical enterprise architecture might look like this:
Angular Application
|
↓
ASP.NET Core API
|
↓
Controller
|
↓
CreateOrderCommand
|
↓
Mediator
|
↓
CreateOrderCommandHandler
|
+-------------+-------------+
↓ ↓
Domain Service Validation
|
↓
Order Repository
|
↓
SQL Server
For asynchronous processing:
Angular
↓
ASP.NET Core API
↓
Command
↓
Azure Service Bus / RabbitMQ
↓
Consumer
↓
Command Handler
↓
Business Logic
↓
Database
This is where the Command Pattern becomes particularly powerful in enterprise applications.
27. Command Pattern – Key Takeaways
Remember these five points:
1. Command represents an operation
CreateOrderCommand
2. Handler executes the operation
CreateOrderCommandHandler
3. Receiver contains the actual business capability
OrderService
4. Commands can be stored, queued, logged, and retried
Command
↓
Queue
↓
Worker
↓
Handler
5. Command is a major building block for CQRS
Command
↓
Handler
↓
Write Model
Conclusion
The Command Design Pattern transforms an operation into an object.
Instead of tightly coupling a caller to the operation:
Caller → Service Method
we can create a more flexible architecture:
Caller
↓
Command
↓
Handler
↓
Receiver / Domain Service
This separation enables powerful capabilities such as:
CQRS
Undo/Redo
Queue-based processing
Background jobs
Auditing
Logging
Retry mechanisms
Asynchronous processing
Distributed command processing
For modern C# and ASP.NET Core applications, the Command Pattern is particularly valuable when an application has complex business operations, multiple execution paths, asynchronous workflows, or CQRS-based architecture.
The key idea: Encapsulate "what needs to be done" as an object, separate it from "how it is done," and gain the flexibility to execute, queue, log, retry, or undo that operation.
🚀 Coming Up Next: Part 4.3 – Interpreter Design Pattern
In the next article, we'll explore the Interpreter Design Pattern, including:
What is the Interpreter Pattern?
Why do we need it?
Expression and Grammar concepts
Terminal and Non-Terminal Expressions
UML Class Diagram
Complete C# Console Application
Building a simple expression interpreter
ASP.NET Core implementation
Business rule evaluation
Search/filter expression example
Real-world enterprise scenarios
Advantages and disadvantages
Best practices
Common mistakes
Interpreter vs Strategy
Interpreter vs Composite
Interpreter vs Specification Pattern
Interview questions
We'll also examine why the Interpreter Pattern is useful for rule engines, expression evaluation, filtering, query languages, and configurable business rules, and how it relates to expression trees and modern .NET applications.
