Understanding CQRS and MediatR in .NET for Better Architecture

In the world of software architecture, achieving a clean, maintainable, and scalable system is paramount. Two patterns that significantly contribute to this goal are Command Query Responsibility Segregation (CQRS) and the MediatR library in .NET. This article delves into these concepts, explaining their principles, benefits, and how they can be implemented in .NET applications.

What is CQRS?

CQRS stands for Command Query Responsibility Segregation. It is an architectural pattern that separates the read and write operations of a system into two distinct models. This separation allows each model to be optimized for its specific purpose, leading to simpler and more scalable architectures, especially in complex systems.

Principles of CQRS

  1. Separation of Concerns: CQRS divides the system into two parts:

    • Commands: Operations that change the state of the system (e.g., create, update, delete).

    • Queries: Operations that retrieve data without modifying the state.

  2. Independent Scaling: By separating read and write operations, each side can be scaled independently based on its load.

  3. Optimized Data Models: The read model can be optimized for queries, while the write model can be optimized for updates.

  4. Improved Performance: CQRS can enhance performance by reducing the complexity of queries and allowing for more efficient data retrieval.

Implementing CQRS in .NET

To implement CQRS in a .NET application, you can use the MediatR library, which facilitates the separation of commands and queries.

What is MediatR?

MediatR is a popular .NET library that implements the Mediator pattern. It helps in decoupling the components of a system by providing a central point for handling requests and responses. MediatR supports both CQRS and the Mediator pattern, making it an excellent choice for implementing these architectural principles in .NET applications.

Benefits of Using MediatR

  • Loose Coupling: Reduces direct dependencies between components, leading to a more modular and maintainable codebase.

  • Separation of Concerns: Separates business logic from the infrastructure, making the system easier to understand and maintain.

  • Extensibility: Allows for easy addition of new features without modifying existing code.

Setting Up MediatR in a .NET Application

Project Setup

  1. Create a New ASP.NET Core Web Application: Open Visual Studio and create a new ASP.NET Core Web Application, selecting API as the project type.

  2. Install MediatR Packages: Install the necessary packages via the Package Manager Console:

     shellCopy codeInstall-Package MediatR
     Install-Package MediatR.Extensions.Microsoft.DependencyInjection
    
  3. Configure MediatR: In the Startup.cs or Program.cs file, add the following code to configure MediatR:

     csharpCopy codeservices.AddMediatR(typeof(Startup).Assembly);
    

Implementing CQRS with MediatR

Abstractions for Commands and Queries

To make the implementation more generic and reusable, define abstractions for commands and queries.

Define ICommand and ICommandHandler

csharpCopy codepublic interface ICommand<out TResponse> : IRequest<TResponse>
{
}

public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse>
    where TCommand : ICommand<TResponse>
{
}

Define IQuery and IQueryHandler

csharpCopy codepublic interface IQuery<out TResponse> : IRequest<TResponse>
{
}

public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse>
    where TQuery : IQuery<TResponse>
{
}

Commands and Command Handlers

Commands represent actions that change the state of the system. Each command has a corresponding handler containing the business logic for processing the command.

Create a Command

Define a command to create a new course.

csharpCopy codepublic class CreateCourseCommand : ICommand<Guid>
{
    public string Title { get; set; }
    public string Description { get; set; }
}

Create a Command Handler

Implement the handler for the command.

csharpCopy codepublic class CreateCourseCommandHandler : ICommandHandler<CreateCourseCommand, Guid>
{
    private readonly ICourseRepository _courseRepository;

    public CreateCourseCommandHandler(ICourseRepository courseRepository)
    {
        _courseRepository = courseRepository;
    }

    public async Task<Guid> Handle(CreateCourseCommand request, CancellationToken cancellationToken)
    {
        var courseId = Guid.NewGuid();

        var course = new Course
        {
            Id = courseId,
            Title = request.Title,
            Description = request.Description
        };

        // Save course to the database
        await _courseRepository.AddAsync(course, cancellationToken);

        return courseId;
    }
}

Queries and Query Handlers

Queries retrieve data without modifying the state. Each query has a corresponding handler that contains the logic for fetching the data.

Create a Query

Define a query to get a course by its ID.

csharpCopy codepublic class GetCourseByIdQuery : IQuery<Course>
{
    public Guid CourseId { get; set; }
}

Create a Query Handler

Implement the handler for the query.

csharpCopy codepublic class GetCourseByIdQueryHandler : IQueryHandler<GetCourseByIdQuery, Course>
{
    private readonly ICourseRepository _courseRepository;

    public GetCourseByIdQueryHandler(ICourseRepository courseRepository)
    {
        _courseRepository = courseRepository;
    }

    public async Task<Course> Handle(GetCourseByIdQuery request, CancellationToken cancellationToken)
    {
        // Get course by ID
        var course = await _courseRepository.GetByIdAsync(request.CourseId, cancellationToken);
        return course;
    }
}

Setting Up API Endpoints

Finally, set up the API endpoints to handle the commands and queries.

Create a Controller

Define a controller to handle the API requests.

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

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

    [HttpPost]
    public async Task<IActionResult> CreateCourse([FromBody] CreateCourseCommand command)
    {
        var courseId = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetCourseById), new { id = courseId }, courseId);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetCourseById(Guid id)
    {
        var query = new GetCourseByIdQuery { CourseId = id };
        var course = await _mediator.Send(query);
        return course is not null ? Ok(course) : NotFound();
    }
}

Conclusion

Implementing CQRS and MediatR in .NET can significantly improve the architecture of your applications by promoting separation of concerns, loose coupling, and scalability. By following the principles outlined in this article, you can create a clean, maintainable, and efficient system well-suited to handle complex business requirements.

For more detailed examples and advanced scenarios, you can refer to the official documentation and various community resources available online. Happy coding!