ASP.NET - CQRS and MediatR Pattern in ASP.NET Core

CQRS stands for Command Query Responsibility Segregation. It is a software design pattern that separates the operations that modify data from the operations that read data. In traditional application architecture, the same model and service classes are often used for both reading and writing data. CQRS divides these responsibilities into two distinct parts:

  • Commands: Used for creating, updating, or deleting data.

  • Queries: Used for retrieving data.

This separation improves scalability, maintainability, readability, and performance in complex enterprise applications.

In ASP.NET Core, CQRS is commonly implemented along with the MediatR library. MediatR is a lightweight library that implements the Mediator design pattern. It helps reduce direct dependencies between controllers and business logic by acting as an intermediary for handling commands and queries.

Why CQRS is Important

As applications grow, combining all operations into a single service layer creates several problems:

  • Large and difficult-to-maintain service classes

  • Tight coupling between components

  • Difficulty in testing

  • Poor scalability

  • Complex business logic handling

CQRS solves these issues by organizing code into separate command and query operations.

For example:

  • A user registration operation belongs to the command side because it modifies data.

  • A user profile retrieval operation belongs to the query side because it only fetches data.

This separation allows developers to optimize read and write operations independently.

Basic Architecture of CQRS

CQRS architecture mainly contains:

Command Side

Responsible for write operations.

Examples:

  • Create Product

  • Update Employee

  • Delete Order

Commands do not return large datasets. Usually they return:

  • Success status

  • ID of created record

  • Simple acknowledgement

Query Side

Responsible for read operations.

Examples:

  • Get Product List

  • Search Customers

  • Fetch Order Details

Queries never modify data.

Introduction to MediatR

MediatR acts as a mediator between different application components. Instead of controllers directly calling services, controllers send commands or queries to MediatR, and MediatR forwards them to the appropriate handlers.

Without MediatR:

  • Controller directly depends on service classes

With MediatR:

  • Controller only depends on IMediator

  • Business logic stays inside handlers

This creates loose coupling and cleaner architecture.

Installing MediatR in ASP.NET Core

The MediatR package can be installed using NuGet Package Manager.

Common packages:

  • MediatR

  • MediatR.Extensions.Microsoft.DependencyInjection

Package Manager Console command:

Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection

Registering MediatR in ASP.NET Core

In Program.cs:

builder.Services.AddMediatR(typeof(Program));

This registers all handlers automatically.

Creating a Command

Suppose we want to create a new employee.

CreateEmployeeCommand.cs

using MediatR;

public class CreateEmployeeCommand : IRequest<int>
{
    public string Name { get; set; }
    public string Department { get; set; }
    public decimal Salary { get; set; }
}

Explanation:

  • IRequest means the command returns an integer value.

  • Usually this integer represents the created employee ID.

Creating a Command Handler

CreateEmployeeCommandHandler.cs

using MediatR;

public class CreateEmployeeCommandHandler 
    : IRequestHandler<CreateEmployeeCommand, int>
{
    public async Task<int> Handle(
        CreateEmployeeCommand request,
        CancellationToken cancellationToken)
    {
        // Database save logic

        int employeeId = 101;

        return employeeId;
    }
}

Explanation:

  • IRequestHandler processes the command.

  • Handle method contains business logic.

  • Command handler performs write operations.

Creating a Query

Now create a query to fetch employee details.

GetEmployeeQuery.cs

using MediatR;

public class GetEmployeeQuery : IRequest<Employee>
{
    public int Id { get; set; }

    public GetEmployeeQuery(int id)
    {
        Id = id;
    }
}

Creating a Query Handler

GetEmployeeQueryHandler.cs

using MediatR;

public class GetEmployeeQueryHandler 
    : IRequestHandler<GetEmployeeQuery, Employee>
{
    public async Task<Employee> Handle(
        GetEmployeeQuery request,
        CancellationToken cancellationToken)
    {
        return new Employee
        {
            Id = request.Id,
            Name = "John",
            Department = "IT",
            Salary = 50000
        };
    }
}

Explanation:

  • Query handler only retrieves data.

  • No update or insert operation should occur here.

Using MediatR in Controller

EmployeeController.cs

using MediatR;
using Microsoft.AspNetCore.Mvc;

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

    public EmployeeController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> CreateEmployee(
        CreateEmployeeCommand command)
    {
        var employeeId = await _mediator.Send(command);

        return Ok(employeeId);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetEmployee(int id)
    {
        var employee = await _mediator.Send(
            new GetEmployeeQuery(id));

        return Ok(employee);
    }
}

Explanation:

  • Controller does not contain business logic.

  • Controller only sends requests to MediatR.

  • MediatR routes requests to proper handlers.

Advantages of CQRS with MediatR

Clean Separation of Concerns

Read and write logic remain independent.

Better Maintainability

Each handler contains focused functionality.

Improved Scalability

Read operations can be optimized separately from write operations.

Easier Testing

Handlers can be unit tested independently.

Reduced Coupling

Controllers do not directly depend on service implementations.

Better Organization

Large applications become more structured and manageable.

Pipeline Behaviors in MediatR

MediatR supports pipeline behaviors similar to middleware.

They allow execution of logic before or after handlers.

Common uses:

  • Logging

  • Validation

  • Exception handling

  • Performance monitoring

  • Authentication

Example:

public class LoggingBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
{
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        Console.WriteLine("Request received");

        var response = await next();

        Console.WriteLine("Response sent");

        return response;
    }
}

CQRS in Real-World Applications

CQRS is widely used in:

  • Banking systems

  • E-commerce platforms

  • ERP applications

  • Healthcare systems

  • Microservices architecture

  • Large enterprise applications

These systems usually have:

  • Complex business rules

  • High scalability requirements

  • Large volumes of data

  • Multiple independent services

CQRS vs Traditional CRUD

Feature Traditional CRUD CQRS
Model Usage Same model for read/write Separate models
Complexity Simple Moderate to High
Scalability Limited High
Maintenance Difficult in large apps Easier
Performance Optimization Limited Better
Suitable For Small applications Enterprise systems

Challenges of CQRS

Although CQRS provides many benefits, it also introduces complexity.

Increased Code Files

Each operation may require:

  • Command/query

  • Handler

  • Validator

  • DTO

Learning Curve

Developers must understand:

  • Mediator pattern

  • Separation principles

  • Messaging concepts

Overengineering Risk

CQRS may not be suitable for very small applications.

For small CRUD applications, traditional architecture may be simpler and more efficient.

Best Practices

Keep Commands Focused

Each command should perform one operation only.

Avoid Business Logic in Controllers

Place all business rules inside handlers.

Use Validation Pipelines

Validate requests before processing.

Separate Read Models and Write Models

Optimize queries independently.

Use Async Operations

Improve scalability using asynchronous programming.

Organize by Features

Structure folders by feature rather than technical layers.

Example:

Features
 ├── Employees
 │    ├── Commands
 │    ├── Queries
 │    ├── Handlers
 │    └── Validators

Conclusion

CQRS with MediatR is a powerful architectural approach in ASP.NET Core applications. It separates read and write responsibilities, improves maintainability, simplifies testing, and creates a cleaner project structure. MediatR further enhances the architecture by reducing dependencies and centralizing request handling.

While CQRS may introduce additional complexity, it becomes extremely valuable in medium and large-scale enterprise applications where scalability, maintainability, and clean architecture are important.