CQRS Pattern in .NET: A Beginner-Friendly Guide


CQRS stands for Command Query Responsibility Segregation.

The name sounds complicated, but the basic idea is actually simple:

Commands change data. Queries read data.

Instead of using one service or method to handle both reading and writing, CQRS separates these two responsibilities.

What Problem Does CQRS Solve?

Imagine you are building an e-commerce application.

You might have a service like this:

public class ProductService
{
    public Product GetProduct(int id)
    {
        // Read product information from the database
        return _repository.GetById(id);
    }

    public void UpdateProduct(Product product)
    {
        // Update product information in the database
        _repository.Update(product);
    }
}

There is nothing wrong with this approach for a small application.

But as the application grows, the same service may start handling many different responsibilities:

  • Creating products
  • Updating products
  • Deleting products
  • Searching products
  • Getting product details
  • Generating reports
  • Validating business rules

The service can become large and difficult to maintain.

CQRS suggests separating write operations from read operations.

What Is CQRS?

CQRS divides application operations into two categories:

1. Command

A Command represents an operation that changes the application's state.

Examples:

CreateProduct
UpdateProduct
DeleteProduct
PlaceOrder
CancelOrder
RegisterCustomer

A command usually contains the information required to perform an operation.

For example:

public class CreateProductCommand
{
    // Name of the product we want to create
    public string Name { get; set; }

    // Price of the product
    public decimal Price { get; set; }
}

The command itself doesn't necessarily perform the operation.

It is simply a message saying:

"Please create this product."

2. Query

A Query is used to retrieve data.

Examples:

GetProductById
GetAllProducts
SearchProducts
GetCustomerOrders
GetOrderDetails

A query should generally not change application state.

For example:

public class GetProductQuery
{
    // ID of the product we want to retrieve
    public int ProductId { get; set; }
}

The query says:

"Please give me the product with this ID."

Simple Way to Remember CQRS

Think about a restaurant.

You have two different activities:

Ordering food

Customer
   |
   v
"Give me a Pizza"
   |
   v
Kitchen

This changes the state of the restaurant because an order has been created.

That's similar to a Command.

Now imagine asking:

Customer
   |
   v
"What is my order status?"
   |
   v
System

You are only reading information.

That's similar to a Query.

So remember:

COMMAND = CHANGE

QUERY = READ

CQRS Architecture

A simple CQRS architecture can look like this:

                Client
                  |
                  v
          ASP.NET Core API
                  |
          +-------+-------+
          |               |
          v               v
      Commands         Queries
          |               |
          v               v
   Command Handler   Query Handler
          |               |
          v               v
       Database         Database

The important part is that the application has separate paths for reading and writing.

CQRS Without MediatR

You don't need a library to understand or implement CQRS.

Let's create a simple example without MediatR first.

Suppose we have a product API.

Our model:

public class Product
{
    // Unique identifier of the product
    public int Id { get; set; }

    // Product name
    public string Name { get; set; }

    // Product price
    public decimal Price { get; set; }
}

Now let's create a command.

CreateProductCommand

public class CreateProductCommand
{
    // Name provided by the client
    public string Name { get; set; }

    // Price provided by the client
    public decimal Price { get; set; }
}

Then we create a command handler.

CreateProductCommandHandler

public class CreateProductCommandHandler
{
    private readonly AppDbContext _dbContext;

    public CreateProductCommandHandler(AppDbContext dbContext)
    {
        // Store the database context for later use
        _dbContext = dbContext;
    }

    public async Task<int> Handle(CreateProductCommand command)
    {
        // Convert the command into a Product entity
        var product = new Product
        {
            Name = command.Name,
            Price = command.Price
        };

        // Add the product to the database
        _dbContext.Products.Add(product);

        // Save the changes
        await _dbContext.SaveChangesAsync();

        // Return the newly generated product ID
        return product.Id;
    }
}

The responsibility is clear:

CreateProductCommand
        |
        v
CreateProductCommandHandler
        |
        v
Create Product
        |
        v
Database

Creating a Query

Now let's retrieve a product.

First, create the query:

public class GetProductQuery
{
    // ID of the product we want to retrieve
    public int ProductId { get; set; }
}

Then create the query handler:

public class GetProductQueryHandler
{
    private readonly AppDbContext _dbContext;

    public GetProductQueryHandler(AppDbContext dbContext)
    {
        // Store the database context
        _dbContext = dbContext;
    }

    public async Task<Product?> Handle(GetProductQuery query)
    {
        // Find the product using the ID from the query
        return await _dbContext.Products
            .FirstOrDefaultAsync(x => x.Id == query.ProductId);
    }
}

Notice something important.

The query handler only retrieves data.

It doesn't update the product.

Using CQRS in a Controller

Now we can use our handlers from an ASP.NET Core controller.

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly CreateProductCommandHandler _createHandler;
    private readonly GetProductQueryHandler _queryHandler;

    public ProductsController(
        CreateProductCommandHandler createHandler,
        GetProductQueryHandler queryHandler)
    {
        // Inject the command handler
        _createHandler = createHandler;

        // Inject the query handler
        _queryHandler = queryHandler;
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductCommand command)
    {
        // Execute the command to create a product
        var productId = await _createHandler.Handle(command);

        // Return the newly created product ID
        return Ok(productId);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id)
    {
        // Create a query containing the requested product ID
        var query = new GetProductQuery
        {
            ProductId = id
        };

        // Execute the query
        var product = await _queryHandler.Handle(query);

        // Return 404 when the product doesn't exist
        if (product == null)
        {
            return NotFound();
        }

        // Return the product to the client
        return Ok(product);
    }
}

Now our API has a clear separation:

POST /api/products
        |
        v
CreateProductCommand
        |
        v
CreateProductCommandHandler
        |
        v
Database


GET /api/products/10
        |
        v
GetProductQuery
        |
        v
GetProductQueryHandler
        |
        v
Database

CQRS With MediatR

In real-world .NET applications, you will often see CQRS implemented using a mediator library such as MediatR.

The mediator provides a central mechanism for sending commands and queries to their handlers.

The architecture becomes:

Controller
    |
    v
Mediator
    |
    +----> Command Handler
    |
    +----> Query Handler

The controller doesn't need to know which handler will process the request.

Installing MediatR

You can install MediatR using the .NET CLI:

dotnet add package MediatR

Then configure it in your application.

For modern ASP.NET Core applications, the exact registration syntax can vary by MediatR version, so check the version's documentation when setting up a new project.

Creating a Command With MediatR

Our command can implement IRequest<T>.

using MediatR;

public class CreateProductCommand : IRequest<int>
{
    // Name of the product
    public string Name { get; set; }

    // Price of the product
    public decimal Price { get; set; }
}

IRequest<int> means:

This request will eventually return an integer.

In our example, that integer will be the new product's ID.

Creating the Command Handler

using MediatR;

public class CreateProductCommandHandler
    : IRequestHandler<CreateProductCommand, int>
{
    private readonly AppDbContext _dbContext;

    public CreateProductCommandHandler(AppDbContext dbContext)
    {
        // Store the database context
        _dbContext = dbContext;
    }

    public async Task<int> Handle(
        CreateProductCommand command,
        CancellationToken cancellationToken)
    {
        // Create a new Product entity from the command
        var product = new Product
        {
            Name = command.Name,
            Price = command.Price
        };

        // Add the product to the database context
        _dbContext.Products.Add(product);

        // Persist the product in the database
        await _dbContext.SaveChangesAsync(cancellationToken);

        // Return the generated product ID
        return product.Id;
    }
}

Now the mediator knows that:

CreateProductCommand
        |
        v
CreateProductCommandHandler

Creating a Query With MediatR

We can create a query in a similar way.

using MediatR;

public class GetProductQuery : IRequest<Product?>
{
    // ID of the product we want to retrieve
    public int ProductId { get; set; }
}

Then create the query handler:

using MediatR;
using Microsoft.EntityFrameworkCore;

public class GetProductQueryHandler
    : IRequestHandler<GetProductQuery, Product?>
{
    private readonly AppDbContext _dbContext;

    public GetProductQueryHandler(AppDbContext dbContext)
    {
        // Store the database context
        _dbContext = dbContext;
    }

    public async Task<Product?> Handle(
        GetProductQuery query,
        CancellationToken cancellationToken)
    {
        // Query the database for the requested product
        return await _dbContext.Products
            .AsNoTracking()
            .FirstOrDefaultAsync(
                x => x.Id == query.ProductId,
                cancellationToken);
    }
}

Notice the use of:

AsNoTracking()

For read-only queries, this can be useful because Entity Framework Core doesn't need to track changes to the returned entity.

Using MediatR in the Controller

Our controller becomes simpler.

using MediatR;

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;

    public ProductsController(IMediator mediator)
    {
        // Store the mediator instance
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> Create(
        CreateProductCommand command)
    {
        // Send the command to the appropriate handler
        var productId = await _mediator.Send(command);

        // Return the newly created product ID
        return Ok(productId);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id)
    {
        // Create a query containing the product ID
        var query = new GetProductQuery
        {
            ProductId = id
        };

        // Send the query to the appropriate handler
        var product = await _mediator.Send(query);

        // Return 404 if the product doesn't exist
        if (product == null)
        {
            return NotFound();
        }

        // Return the product
        return Ok(product);
    }
}

Now the controller doesn't directly communicate with command/query handlers.

It communicates with the mediator.

Typical CQRS Project Structure

A common project structure might look like this:

MyApplication
│
├── Controllers
│   └── ProductsController.cs
│
├── Features
│   └── Products
│       │
│       ├── Commands
│       │   └── CreateProduct
│       │       ├── CreateProductCommand.cs
│       │       └── CreateProductCommandHandler.cs
│       │
│       └── Queries
│           └── GetProduct
│               ├── GetProductQuery.cs
│               └── GetProductQueryHandler.cs
│
├── Domain
│   └── Product.cs
│
├── Infrastructure
│   └── AppDbContext.cs
│
└── Program.cs

This structure makes it easier to locate functionality.

If you want to understand how a product is created, you can go directly to:

Features
   |
   +-- Products
         |
         +-- Commands
               |
               +-- CreateProduct

CQRS Does NOT Require Two Databases

This is an important point for beginners.

Many people think CQRS means:

Command Database
       +
Query Database

That's not necessarily true.

You can implement CQRS using a single database:

Commands ──────┐
               |
               v
          SQL Database
               ^
               |
Queries ───────┘

This is often a good starting point.

More advanced systems can use separate read and write databases:

                 Application
                /            \
               /              \
        Commands              Queries
           |                     |
           v                     v
    Write Database          Read Database
           |                     ^
           |                     |
           +---- Events ---------+

This is sometimes called CQRS with separate read and write models.

CQRS and Event Sourcing Are Different

Another common beginner misconception is:

CQRS = Event Sourcing

They are related concepts, but they are not the same thing.

CQRS means:

Separate READ responsibilities
from
WRITE responsibilities

Event Sourcing means:

Store changes as a sequence of events
rather than only storing the current state.

You can use:

CQRS without Event Sourcing

and:

Event Sourcing without full CQRS

Although they are frequently used together in complex systems.

Advantages of CQRS

1. Clear separation of responsibilities

  • Commands modify data.
  • Queries retrieve data.
  • This makes the code easier to understand.

2. Easier maintenance

Instead of having one huge service:

ProductService
 ├── Create
 ├── Update
 ├── Delete
 ├── Search
 ├── GetById
 ├── Reports
 └── Statistics

you can organize functionality around individual use cases:

CreateProductCommand
UpdateProductCommand
DeleteProductCommand

GetProductQuery
SearchProductsQuery
GetProductStatisticsQuery

3. Better scalability

Read and write workloads can be very different.

For example:

10,000 READ requests
        +
100 WRITE requests
  • You may eventually want to scale the read side independently.
  • CQRS makes that architectural direction easier.

4. Better testing

Individual handlers can be tested independently.

For example:

CreateProductCommandHandlerTests
GetProductQueryHandlerTests
UpdateProductCommandHandlerTests

Each test can focus on one use case.

5. Cleaner business logic

Complex business operations can live inside command handlers or the domain layer rather than becoming mixed into controllers.

Disadvantages of CQRS

  • CQRS isn't automatically better.
  • It also introduces complexity.

1. More classes

A simple CRUD operation might require:

Command
CommandHandler
Query
QueryHandler
DTO
Validator

For a small application, this may feel excessive.

2. More architecture to learn

Developers need to understand:

  • Commands
  • Queries
  • Handlers
  • Mediators
  • DTOs
  • Domain logic
  • Dependency injection

That's more concepts than a simple service-based architecture.

3. Potential overengineering

If your application is simply:

Create Customer
Get Customer
Update Customer
Delete Customer
  • traditional CRUD may be perfectly adequate.
  • You don't need CQRS just because it is a popular architecture.

When Should You Use CQRS?

CQRS can be a good choice when:

  • Your business logic is complex.
  • Read and write operations have very different requirements.
  • The application is large.
  • Different teams work on different parts of the system.
  • You need independent scaling of read/write workloads.
  • You have many business use cases.
  • You want a clear use-case-oriented architecture.

For example:

Banking System
E-commerce Platform
Order Management
Inventory System
Large Enterprise Applications
Financial Applications

These systems can benefit from CQRS.

When Should You Avoid CQRS?

For a small application such as:

Simple Blog
Small Admin Panel
Basic CRUD API
Small Internal Tool

traditional CRUD may be easier and more maintainable.

A simple architecture like:

Controller
    |
    v
Service
    |
    v
Repository
    |
    v
Database

might be all you need.

CQRS vs Traditional CRUD

Here's a simple comparison:

Traditional CRUD CQRS
Read/write logic often shares services Reads and writes are separated
Simple to implement More structured
Less code More classes/code
Great for simple applications Useful for complex applications
Usually one model Can use separate read/write models
Easier for beginners Better for complex business workflows

The Most Important Concept

Don't get distracted by libraries such as MediatR.

MediatR is just a tool.

The core CQRS idea is:

              CQRS
                |
       +--------+--------+
       |                 |
       v                 v
   COMMAND            QUERY
       |                 |
       v                 v
    CHANGE             READ
     DATA              DATA

Once you understand this, libraries and frameworks become much easier to understand.

A Real-World Example

Imagine an online shopping application.

A customer clicks "Place Order."

That's a command:

PlaceOrderCommand

The handler performs business operations:

Validate Order
      |
      v
Check Inventory
      |
      v
Calculate Total
      |
      v
Create Order
      |
      v
Save Order

Later, the customer opens the order page.

That's a query:

GetOrderDetailsQuery

The query might return:

{
  "orderId": 1001,
  "customerName": "John",
  "total": 2500,
  "status": "Confirmed"
}

The important distinction is:

PlaceOrderCommand
       ↓
Changes system state


GetOrderDetailsQuery
       ↓
Reads system state

Final Takeaway

CQRS stands for Command Query Responsibility Segregation.

The basic rule is extremely simple:

Commands change data; Queries read data.

In a .NET application, you can implement CQRS manually or use tools such as MediatR to dispatch commands and queries to their handlers.

A typical flow looks like:

HTTP Request
     |
     v
Controller
     |
     v
Command / Query
     |
     v
Handler
     |
     v
Business Logic
     |
     v
Database

The biggest lesson for a beginner is not to introduce CQRS everywhere.

Start with simple CRUD.

When your application becomes more complex and your read/write responsibilities need clearer separation, CQRS can provide a useful structure.

Once you understand the fundamental idea of "Commands change, Queries read," the rest of CQRS becomes much easier to learn.

0 Comments Report