Tuesday, July 28, 2026

Modern C# Features

Modern C# Features – Detailed Guide with Real-Time Examples

These features are especially important in modern C# and ASP.NET Core development:

  1. Async / Await

  2. Pattern Matching

  3. Nullable Reference Types

  4. Records

  5. Tuples

  6. Expression-Bodied Members

  7. Local Functions

  8. LINQ

  9. Generics

  10. init Properties

  11. required Members

  12. Improved Pattern Matching


1. Async / Await

What is Async/Await?

async and await are used to write asynchronous code.

They are especially useful when your application needs to wait for:

  • Database calls

  • REST API calls

  • File operations

  • Azure services

  • External services

  • Network operations

Instead of blocking a thread while waiting, asynchronous programming allows the application to do other work.

Synchronous example

public string GetCustomer()
{
    var customer = database.GetCustomer();
    return customer;
}

If the database takes 3 seconds, the thread waits for 3 seconds.

Asynchronous example

public async Task<string> GetCustomerAsync()
{
    var customer = await database.GetCustomerAsync();
    return customer;
}

The application can use resources more efficiently while waiting for I/O.


Real-Time ASP.NET Core Example

Suppose we have a customer API.

[HttpGet("{id}")]
public async Task<IActionResult> GetCustomer(int id)
{
    var customer = await _customerService.GetCustomerAsync(id);

    if (customer == null)
        return NotFound();

    return Ok(customer);
}

Service:

public async Task<Customer?> GetCustomerAsync(int id)
{
    return await _context.Customers
        .FirstOrDefaultAsync(x => x.Id == id);
}

Repository/database call:

var customer = await _context.Customers
    .FirstOrDefaultAsync(x => x.Id == id);

Why is this important?

Imagine 1,000 users calling your API.

Blocking threads while waiting for database/network operations can reduce scalability.

Async programming helps ASP.NET Core handle I/O-heavy workloads efficiently.


Task vs Task<T>

Task

Used when a method doesn't return a value.

public async Task SendEmailAsync()
{
    await emailService.SendAsync();
}

Task

Used when a method returns a value.

public async Task<Customer> GetCustomerAsync()
{
    return await repository.GetCustomerAsync();
}

Important interview question

Q: Does async/await create a new thread?

Not necessarily.

For I/O-bound operations, await generally allows the current thread to be released while the operation completes. When the operation finishes, execution continues.


2. Pattern Matching

Pattern matching allows you to check an object's type, value, structure, or properties more elegantly.

Traditional code:

if (customer != null)
{
    if (customer.Age >= 18)
    {
        // Adult
    }
}

Pattern matching:

if (customer is { Age: >= 18 })
{
    // Adult
}

Type Pattern

object value = "Hello";

if (value is string text)
{
    Console.WriteLine(text.Length);
}

Here:

value is string text

checks:

  1. Is value a string?

  2. If yes, assign it to text.


Real-Time Example

Suppose you have different payment types:

public abstract class Payment
{
}

public class CreditCardPayment : Payment
{
    public decimal Amount { get; set; }
}

public class UpiPayment : Payment
{
    public decimal Amount { get; set; }
}

You can process them using pattern matching:

public void ProcessPayment(Payment payment)
{
    if (payment is CreditCardPayment card)
    {
        Console.WriteLine($"Credit Card: {card.Amount}");
    }
    else if (payment is UpiPayment upi)
    {
        Console.WriteLine($"UPI: {upi.Amount}");
    }
}

3. Nullable Reference Types

Nullable reference types help prevent NullReferenceException.

Before nullable reference types:

string name = null;

The compiler doesn't necessarily warn you about this.

Modern C#:

string name = "Mahesh";

means name shouldn't be null.

If null is valid:

string? name = null;

The ? tells the compiler:

This variable is allowed to contain null.


Real-Time Example

Consider:

public Customer GetCustomer(int id)
{
    return repository.GetCustomer(id);
}

What if the customer doesn't exist?

The repository may return null.

Better:

public Customer? GetCustomer(int id)
{
    return repository.GetCustomer(id);
}

Then:

Customer? customer = GetCustomer(100);

if (customer != null)
{
    Console.WriteLine(customer.Name);
}

Or:

Console.WriteLine(customer?.Name);

Null-Coalescing Operator

string displayName = customer?.Name ?? "Unknown Customer";

Meaning:

If customer or Name is null, use "Unknown Customer".


Real Enterprise Scenario

API:

[HttpGet("{id}")]
public async Task<IActionResult> GetCustomer(int id)
{
    Customer? customer = await _service.GetCustomerAsync(id);

    if (customer is null)
        return NotFound();

    return Ok(customer);
}

This makes nullability explicit and improves code safety.


4. Records

Records are useful for representing data, particularly immutable data.

Traditional class:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Record:

public record Customer(int Id, string Name);

That's much shorter.


Record Equality

This is one of the major differences.

Classes generally use reference equality unless equality is overridden.

Records provide value-based equality.

var customer1 = new Customer(1, "John");
var customer2 = new Customer(1, "John");

Console.WriteLine(customer1 == customer2);

For a record, this evaluates to:

True

because the values are equal.


Real-Time API DTO

Records are excellent for immutable request/response models.

public record CustomerResponse(
    int Id,
    string Name,
    string Email);

Controller:

[HttpGet("{id}")]
public async Task<CustomerResponse?> GetCustomer(int id)
{
    return await _service.GetCustomerAsync(id);
}

with Expression

Records support non-destructive modification.

var customer1 = new Customer(1, "John");

var customer2 = customer1 with
{
    Name = "David"
};

Original object remains unchanged.


5. Tuples

Tuples allow you to return multiple values from a method without creating a separate class.

Instead of:

public class CustomerResult
{
    public string Name { get; set; }
    public decimal Balance { get; set; }
}

You can write:

public (string Name, decimal Balance) GetCustomerDetails()
{
    return ("John", 5000);
}

Usage:

var result = GetCustomerDetails();

Console.WriteLine(result.Name);
Console.WriteLine(result.Balance);

Real-Time Banking Example

public (bool Success, decimal Balance, string Message)
    Withdraw(decimal amount)
{
    decimal balance = 5000;

    if (amount > balance)
    {
        return (false, balance, "Insufficient balance");
    }

    balance -= amount;

    return (true, balance, "Withdrawal successful");
}

Usage:

var result = Withdraw(1000);

if (result.Success)
{
    Console.WriteLine(result.Balance);
}
else
{
    Console.WriteLine(result.Message);
}

Tuple Deconstruction

var (success, balance, message) = Withdraw(1000);

Very useful for methods returning multiple related values.


6. Expression-Bodied Members

Expression-bodied members allow you to write short methods and properties using =>.

Traditional:

public string GetFullName()
{
    return FirstName + " " + LastName;
}

Expression-bodied:

public string GetFullName() =>
    FirstName + " " + LastName;

Property Example

Traditional:

public string FullName
{
    get
    {
        return FirstName + " " + LastName;
    }
}

Expression-bodied:

public string FullName =>
    FirstName + " " + LastName;

Real-Time Example

public class Product
{
    public decimal Price { get; set; }

    public decimal Tax =>
        Price * 0.18m;

    public decimal FinalPrice =>
        Price + Tax;
}

Usage:

var product = new Product
{
    Price = 1000
};

Console.WriteLine(product.FinalPrice);

Expression-bodied members are best when the logic is short and obvious.


7. Local Functions

A local function is a method defined inside another method.

Example:

public void ProcessOrder(Order order)
{
    bool IsValid()
    {
        return order != null &&
               order.Items.Count > 0;
    }

    if (IsValid())
    {
        Console.WriteLine("Order is valid");
    }
}

IsValid() is accessible only inside ProcessOrder.


Why use Local Functions?

They are useful when:

  • Logic is needed only by one method

  • You want to improve readability

  • You want to keep helper logic private to a particular operation

  • You don't want to create another class-level method


Real-Time Example

public decimal CalculateOrderTotal(Order order)
{
    decimal CalculateItemTotal(OrderItem item)
    {
        return item.Price * item.Quantity;
    }

    return order.Items.Sum(CalculateItemTotal);
}

The helper function is relevant only to this calculation.


8. LINQ

LINQ = Language Integrated Query.

It allows you to query:

  • Collections

  • Arrays

  • Lists

  • Databases

  • XML

  • Objects


Without LINQ

var expensiveProducts = new List<Product>();

foreach (var product in products)
{
    if (product.Price > 1000)
    {
        expensiveProducts.Add(product);
    }
}

LINQ:

var expensiveProducts = products
    .Where(p => p.Price > 1000)
    .ToList();

Real-Time E-Commerce Example

Suppose:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
}

Find products above $1,000:

var products = db.Products
    .Where(p => p.Price > 1000)
    .ToList();

Sort:

var products = db.Products
    .Where(p => p.Price > 1000)
    .OrderByDescending(p => p.Price)
    .ToList();

Select only required fields:

var products = db.Products
    .Where(p => p.Price > 1000)
    .Select(p => new
    {
        p.Name,
        p.Price
    })
    .ToList();

Important LINQ Methods

MethodPurpose
Where()Filtering
Select()Projection
OrderBy()Sorting
OrderByDescending()Reverse sorting
First()First element
FirstOrDefault()First or default
Single()Exactly one element
Any()Checks whether any exists
All()Checks whether all satisfy condition
Count()Count
Sum()Sum
Average()Average
GroupBy()Grouping
Join()Joining
ToList()Materialization

LINQ with EF Core

var customers = await _context.Customers
    .Where(c => c.IsActive)
    .OrderBy(c => c.Name)
    .ToListAsync();

EF Core can translate this LINQ expression into SQL.


9. Generics

Generics allow you to write reusable, type-safe code.

Without generics:

public class CustomerRepository
{
}

You may end up creating:

CustomerRepository
ProductRepository
OrderRepository
EmployeeRepository

Instead, create a generic repository:

public class Repository<T>
{
    public void Add(T entity)
    {
        // Add entity
    }

    public T GetById(int id)
    {
        // Get entity
        return default;
    }
}

Then:

Repository<Customer> customerRepository =
    new Repository<Customer>();

Repository<Product> productRepository =
    new Repository<Product>();

Generic Method

public T GetValue<T>(T value)
{
    return value;
}

Usage:

int number = GetValue(10);

string name = GetValue("John");

Real-Time API Result

A common enterprise approach is:

public class ApiResponse<T>
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public T? Data { get; set; }
}

Then:

ApiResponse<Customer>

or:

ApiResponse<List<Customer>>

or:

ApiResponse<Product>

This gives you reusable API response structures.


10. init Properties

init properties were introduced to make objects easier to initialize while preventing modification afterward.

Traditional:

public class Customer
{
    public int Id { get; set; }
}

You can change it anytime:

customer.Id = 100;

With init:

public class Customer
{
    public int Id { get; init; }
    public string Name { get; init; }
}

Now:

var customer = new Customer
{
    Id = 100,
    Name = "John"
};

But after initialization:

customer.Id = 200;

is not allowed.


Real-Time Example

Consider an order ID.

Once an order object has been created, you don't want random code changing its identity.

public class Order
{
    public int OrderId { get; init; }

    public DateTime OrderDate { get; init; }

    public decimal Amount { get; init; }
}

Create:

var order = new Order
{
    OrderId = 1001,
    OrderDate = DateTime.UtcNow,
    Amount = 250
};

This makes the object safer to work with.


11. required Members

required ensures that a property must be initialized when creating an object.

Example:

public class Customer
{
    public required int Id { get; set; }

    public required string Name { get; set; }

    public string? Email { get; set; }
}

Now:

var customer = new Customer
{
    Id = 1,
    Name = "John"
};

This is valid.

But:

var customer = new Customer();

will generate a compiler error because required properties weren't initialized.


Real-Time Example

Suppose an employee must always have:

  • Employee ID

  • Name

  • Department

public class Employee
{
    public required int EmployeeId { get; init; }

    public required string Name { get; init; }

    public required string Department { get; init; }

    public string? Email { get; init; }
}

Usage:

var employee = new Employee
{
    EmployeeId = 101,
    Name = "John",
    Department = "IT"
};

You cannot accidentally forget required information.


12. Improved Pattern Matching

Modern C# has significantly improved pattern matching.

Important patterns include:

  • Property patterns

  • Relational patterns

  • Logical patterns

  • List patterns

  • Switch expressions


A. Property Pattern

Instead of:

if (customer != null &&
    customer.IsActive &&
    customer.Age >= 18)
{
}

You can write:

if (customer is
{
    IsActive: true,
    Age: >= 18
})
{
    Console.WriteLine("Eligible customer");
}

B. Relational Patterns

You can directly compare values:

if (age is >= 18)
{
    Console.WriteLine("Adult");
}

Multiple conditions:

if (age is >= 18 and <= 60)
{
    Console.WriteLine("Working age");
}

C. Logical Patterns

and

if (salary is > 50000 and < 100000)
{
    Console.WriteLine("Salary is within range");
}

or

if (status is "Pending" or "Processing")
{
    Console.WriteLine("Order is being processed");
}

not

if (status is not "Cancelled")
{
    Console.WriteLine("Order is active");
}

D. Switch Expression

Traditional:

string GetStatus(int status)
{
    switch (status)
    {
        case 1:
            return "Pending";

        case 2:
            return "Approved";

        case 3:
            return "Rejected";

        default:
            return "Unknown";
    }
}

Modern:

string GetStatus(int status) =>
    status switch
    {
        1 => "Pending",
        2 => "Approved",
        3 => "Rejected",
        _ => "Unknown"
    };

This is cleaner and easier to maintain.


E. Real-Time Banking Example

Suppose a bank transaction has different states.

public record Transaction(
    decimal Amount,
    string Status,
    bool IsFraud);

We can determine the result:

string ProcessTransaction(Transaction transaction)
{
    return transaction switch
    {
        { IsFraud: true }
            => "Transaction blocked",

        { Status: "Pending", Amount: > 10000 }
            => "Manual verification required",

        { Status: "Approved" }
            => "Transaction successful",

        { Status: "Rejected" }
            => "Transaction rejected",

        _
            => "Unknown transaction"
    };
}

This is a very good example of modern C# pattern matching in an enterprise application.


13. Putting Multiple Features Together

Now let's combine these features into a realistic Customer Service example.

public record Customer(
    int Id,
    string Name,
    int Age,
    bool IsActive);

Generic API response:

public class ApiResponse<T>
{
    public required bool Success { get; init; }

    public string? Message { get; init; }

    public T? Data { get; init; }
}

Service:

public async Task<ApiResponse<Customer>> GetCustomerAsync(int id)
{
    Customer? customer =
        await GetFromDatabaseAsync(id);

    if (customer is null)
    {
        return new ApiResponse<Customer>
        {
            Success = false,
            Message = "Customer not found"
        };
    }

    if (customer is
        {
            IsActive: true,
            Age: >= 18
        })
    {
        return new ApiResponse<Customer>
        {
            Success = true,
            Message = "Eligible customer",
            Data = customer
        };
    }

    return new ApiResponse<Customer>
    {
        Success = false,
        Message = "Customer is not eligible",
        Data = customer
    };
}

This small example uses:

  • async/await

  • Nullable reference types

  • Records

  • Pattern matching

  • Property patterns

  • Relational patterns

  • required

  • init

  • Generics


14. How These Features Fit Together in a Real .NET Application

Think about a typical enterprise application:

Angular
   ↓
ASP.NET Core Web API
   ↓
Controller
   ↓
Service
   ↓
Repository
   ↓
EF Core
   ↓
SQL Server

Modern C# features can appear throughout the architecture:

FeatureTypical Usage
Async/AwaitAPI, DB, HTTP calls
Pattern MatchingBusiness rules
Nullable Reference TypesNull safety
RecordsDTOs/value objects
TuplesMultiple return values
Expression-bodied membersSimple properties/methods
Local FunctionsSmall internal helper logic
LINQCollections + EF Core
GenericsRepository/API response/service infrastructure
initImmutable object initialization
requiredMandatory object properties
Improved Pattern MatchingBusiness rules/state processing

15. Interview Perspective

For a Senior .NET Developer / .NET Lead, don't just memorize syntax.

Be prepared to explain why and when you use each feature.

For example:

Async/Await

"I use async/await for I/O-bound operations such as database calls, HTTP requests and file operations. It improves scalability by avoiding unnecessary thread blocking."

Records

"I use records primarily for immutable data models, DTOs and value objects where value-based equality is useful."

Nullable Reference Types

"Nullable reference types provide compile-time nullability analysis and help prevent NullReferenceException by explicitly distinguishing nullable and non-nullable references."

Generics

"Generics allow reusable and type-safe implementations. In enterprise applications I use them for repositories, API response wrappers, services and reusable infrastructure."

Pattern Matching

"Pattern matching provides a concise way to perform type, property, relational and structural checks. It is particularly useful for implementing business rules and state-based processing."


16. Most Important Features to Prioritize for Interviews

If you're preparing for a .NET Lead interview, I'd prioritize them like this:

🔴 Must Know

  1. Async/Await

  2. LINQ

  3. Generics

  4. Nullable Reference Types

  5. Pattern Matching

  6. Records

🟠 Very Important

  1. init

  2. required

  3. Tuples

  4. Improved Pattern Matching

🟢 Easy but Useful

  1. Expression-bodied members

  2. Local functions


Recommended Learning Sequence

I recommend studying these in this order:

C# Fundamentals
      ↓
Generics
      ↓
LINQ
      ↓
Async / Await
      ↓
Nullable Reference Types
      ↓
Records
      ↓
Tuples
      ↓
Pattern Matching
      ↓
init / required
      ↓
Advanced Pattern Matching
      ↓
ASP.NET Core Web API
      ↓
EF Core
      ↓
Microservices
      ↓
Azure


Monday, July 27, 2026

Command Design Pattern

 


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

  1. Introduction

  2. What is the Command Design Pattern?

  3. Why Do We Need the Command Pattern?

  4. Command Pattern Terminology

  5. UML Class Diagram

  6. How the Command Pattern Works

  7. Complete C# Console Application

  8. Multiple Commands Example

  9. ASP.NET Core Implementation

  10. Command and Handler Architecture

  11. Command Pattern and CQRS

  12. Undo/Redo Functionality

  13. Queue and Background Processing

  14. Banking Transaction Example

  15. E-Commerce Order Example

  16. Real-World Enterprise Scenarios

  17. Advantages

  18. Disadvantages

  19. Best Practices

  20. Common Mistakes

  21. Command vs Strategy

  22. Command vs Mediator

  23. Command vs Chain of Responsibility

  24. Interview Questions

  25. Summary

  26. 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.

FeatureCommandStrategy
Primary PurposeEncapsulate a requestEncapsulate an algorithm
FocusWhat should be done?How should it be done?
Can be Queued?YesUsually not the main goal
Undo/RedoCommon use caseNot typical
CQRSCommonNot primary
Algorithm SelectionNot primaryCore 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.

FeatureCommandChain of Responsibility
Main GoalEncapsulate a requestPass request through handlers
Primary FocusRepresent an operationProcess through a sequence
QueueVery commonNot the primary purpose
Undo/RedoCommonNot typical
Handler ChainNot requiredCore concept
CQRSCommonNot 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.

Don't Copy

Protected by Copyscape Online Plagiarism Checker