How to implement CQRS without MediatR

September 24 2024

 

Many thanks to the sponsors who make it possible for this newsletter to be free for readers.

 

• Join Postman CTO, Ankit Sobti, and Head of Customer Experience and Success, Kristine Chin, at this webinar which delivers the information you need to maximize the success of your API products, reduce friction to collaboration, and to provide a world-class experience for your developers, partners, and customers.
Join here.

 
 

 
 

Introduction

 
 

CQRS (Command Query Responsibility Segregation) is a design pattern that separates the read (query) and write (command) operations of an application, leading to better maintainability, scalability, and flexibility.

 

It's particularly suitable for applications with complex business logic, high read/write ratio, or a need to scale independently.

 

How it works:

 

• Commands: Operations that change the state of the system. Commands usually don't return data, only the status of the operation.

 

• Queries: Operations that retrieve data from the system. Queries only read data and don't modify the system state.

 
 

CQRS in Microservices and Separate Databases

 
 

When using CQRS in a microservices architecture, you can separate read and write operations within individual services or across multiple services, providing several benefits:

 

• Event Sourcing: Combine CQRS with Event Sourcing for better auditability, data versioning, and troubleshooting.

 

• Separate Data Stores: Maintain separate data stores for read and write sides, optimizing performance and ensuring data consistency.

 

• Independent Scaling: Scale the read and write sides of your microservices independently for optimal resource usage.Using separate databases for reading and writing can offer several advantages:

 

• Optimized Performance: Tailor each database to the specific requirements of your read and write operations.

 

• Independent Scaling: Scale the read and write databases independently based on your application's needs.

 

• Flexibility: Choose the most suitable database technology for each side of your application.

 
 

CQRS + MediatR?

 
 

This is the most common combination of implementation of the CQRS pattern seen on projects of the past few years.

 

But let's see what exactly is MediatR?

 

 

Mediator Pattern: MediatR follows the mediator design pattern, where a central mediator object facilitates communication between different components without them needing to be aware of each other. This reduces the coupling between components and makes the system easier to maintain and evolve.

 

Why do people use MediatR in combination with CQRS?

 

In my opinion, using MediatR with CQRS encourages developers to create dedicated command and query classes and their corresponding handlers, which results in better code organization.

 

Also, MediatR provides a simple request/response model and a central mediator for handling commands, queries, and events. This makes it easier to implement the CQRS pattern in a consistent and straightforward way.

 

But is this really necessary?

 

Of course not.

 

I will show that in the following text.

 
 

Clean CQRS

 
 

The project CQRS has a following structure:

Clean CQRS

To implement the CQRS pattern, without using any libraries, it is necessary to create only 4 interfaces.

 

All 4 interfaces are located in the Common folder:

 

- IQueryHandler - This interface is responsible for handling query operations. It has a single Handle method that takes a query object of type TQuery and a cancellation token, and returns a Task representing the result of the query operation.

public interface IQueryHandler<in TQuery, TQueryResult>
{
    Task<TQueryResult> Handle(TQuery query, CancellationToken cancellation);
}

 

- ICommandHandler - This interface is responsible for handling command operations.

public interface ICommandHandler<in TCommand, TCommandResult>
{
    Task<TCommandResult> Handle(TCommand command, CancellationToken cancellation);
}

 

- IQueryDispatcher - This interface is responsible for dispatching queries to their respective query handlers. It has a generic Dispatch method that takes a query object and a cancellation token, and returns a Task representing the result of the dispatched query.

public interface IQueryDispatcher
{
    Task<TQueryResult> Dispatch<TQuery, TQueryResult>(TQuery query, CancellationToken cancellation);
}

 

- ICommandDispatcher - This interface is responsible for dispatching commands to their respective command handlers.

public interface ICommandDispatcher
{
    Task<TCommandResult> Dispatch<TCommand, TCommandResult>(TCommand command, CancellationToken cancellation);
}

 

In order for dispatchers to know which handlers (queries or commands) they will call, it is necessary to tell them how to select handlers.

 

That is why we will make implementations of both dispatchers. Place them in the Dispatchers folder.

 

QueryDispatcher implementation:

public class QueryDispatcher(IServiceProvider serviceProvider) : IQueryDispatcher
{
    private readonly IServiceProvider _serviceProvider = serviceProvider;

    public Task<TQueryResult> Dispatch<TQuery, TQueryResult>(TQuery query, CancellationToken cancellation)
    {
        var handler = _serviceProvider.GetRequiredService<IQueryHandler<TQuery, TQueryResult>>();
        return handler.Handle(query, cancellation);
    }
}
CommandDispatcher implementation:

public class CommandDispatcher(IServiceProvider serviceProvider) : ICommandDispatcher
{
    private readonly IServiceProvider _serviceProvider = serviceProvider;

    public Task<TCommandResult> Dispatch<TCommand, TCommandResult>(TCommand command, CancellationToken cancellation)
    {
        var handler = _serviceProvider.GetRequiredService<ICommandHandler<TCommand, TCommandResult>>();
        return handler.Handle(command, cancellation);
    }
}
The Dispatch method of the CommandDispatcher class takes two type parameters, TCommand and TCommandResult, and two arguments, command and cancellation, respectively.

 

Within the Dispatch method, the appropriate ICommandHandler<TCommand, TCommandResult> is obtained from the _serviceProvider field using the GetRequiredService method, which returns a new instance of the service.

 

Then the Handle method of the obtained handler is called with the provided command and cancellation arguments. Finally, the Task returned from the Handle method is returned from the Dispatch method.

 
 

How to use it?

 
 

As you would use it with MediatR, you use it in an identical way without the library.

 

Let's say we have a UsersController that represents an endpoint in the API and returns a user with a given Id. Since it queries the database, we know that we will have some Query and QueryHandler.

 

The first thing that needs to be done is to inject the QueryDispatcher (this is how we also inject IMediatR) through DI.

[Route("api/[controller]")]
[ApiController]
public class UsersController(IQueryDispatcher queryDispatcher, ICommandDispatcher commandDispatcher) : ControllerBase
{
    private readonly IQueryDispatcher _queryDispatcher = queryDispatcher;
    private readonly ICommandDispatcher _commandDispatcher = commandDispatcher;

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(Guid id, CancellationToken cancellationToken)
    {
        var query = new GetUserByIdQuery { UserId = id };
        var user = await _queryDispatcher.Dispatch<GetUserByIdQuery, User>(query, cancellationToken);

        if (user == null)
        {
            return NotFound();
        }

        return Ok(user);
    }
}

 

The _queryDispatcher.Dispatch method sends the query to the appropriate query handler registered with the application's dependency injection container, which executes the query and returns the result.

 

In this case, the query handler retrieves the user with the specified ID from the data store and returns it as a User object as result.

 

A similar way to how MediatR calls the corresponding handlers.

 

In order for this to work, it is necessary to create a Query and its Handler.

 

GetUserByIdQuery is nothing but a simple wrapper around Id.

 

For the same Query, there is also a QueryHandler that will be called when the Dispatcher dispatches this Query.

 

The QueryHandler looks like this:

public class GetUserByIdQueryHandler : IQueryHandler<GetUserByIdQuery, User>
{
    public GetUserByIdQueryHandler() { }

    public async Task<User> Handle(GetUserByIdQuery query, CancellationToken cancellationToken)
    {
        //Call Repository
        return new User();
    }
}
For every other command or query, you would create the same class structure.

 

What is missing here is validation, which can be represented as the GetUserByIdQueryValidation class and which will validate the input.

 
 

Wrapping Up

 
 

That's all from me today.

 

I strongly recommend that you read/watch something about the Mediator pattern as well as about the MediatR library itself, where it is most suitable for use.

 

Using MediatR in CQRS implementation is not wrong. Considering the increasing use of 'Clean Architecture', it is a great way to make code more readable and maintainable.

 

I have certainly shown here that the purpose of MediatR is not to be used to implement CQRS, and that it is absolutely possible to implement this pattern without any libraries in a very simple way.

 

It's Monday, make a coffee and check the whole project implementation on GitHub repository.

Join 13,250+ subscribers to improve your .NET Knowledge.

There are 3 ways I can help you:

Design Patterns Simplified ebook

Go-to resource for understanding the core concepts of design patterns without the overwhelming complexity. In this concise and affordable ebook, I've distilled the essence of design patterns into an easy-to-digest format. It is a Beginner level. Check out it here.


Sponsorship

Promote yourself to 13,250+ subscribers by sponsoring this newsletter.


Join TheCodeMan.net Newsletter

Every Monday morning, I share 1 actionable tip on C#, .NET & Arcitecture topic, that you can use right away.


Subscribe to
TheCodeMan.net

Subscribe to the TheCodeMan.net and be among the 13,250+ subscribers gaining practical tips and resources to enhance your .NET expertise.