How CQRS and MediatR Found Love in .Net?

Amel Karsic

Intro

Most of the traditional applications’ read and write database requests are asymmetrical, and database is mostly adapted to only one type of request.

In this blog, I’ll show you how to set the right software architecture so that your system has the best read and write performances. 

To succeed in this mission, all you need is some knowledge about CQRS, and about what it can do with a little help from MeaditR.

CQRS (CQS)

CQRS is short for Command Query Responsibility Segregation, while CQS is short for (Command Query Segregation). CQS is a code pattern that segregates GET response into one flow (query flow) and writing or updating request (POST, PUT, PATCH, DELETE) into another flow (command flow). CQRS is an architectural pattern that requires a read database for query flow and write database for command flow.

Read database can be fully optimized for read (with all indexes that will increase read speed, etc.), while write database is optimized for data updates and insertions.

MediatR

MediatR is the implementation of a mediator pattern wrapped up in a C# library. As you can see in the picture below, our controllers send requests to MediatR. Afterward, MediatR works its magic and finds a matching handler that will provide the right response. MediatR allows us to create and listen to different kinds of events in a specific pattern that is very simple and easy to use (as you will see in code snippets below).

How do MediatR and CQRS fit together?

MediatR fits perfectly in the application layer of the CQRS schema, but don’t just take my word for it. These two combined will give your application the best read-write performances, thin controllers, and handlers that will have a single responsibility.

Without further ado, let’s jump into the code and see how this works in reality.

First, we need to install MediatR nuget packages:

Let’s start with Startup.cs. When using MediatR, we only need to inject MediatR to Startup. No other service injections are needed.

public void ConfigureServices(IServiceCollection services)
        {
            services.AddMediatR(Assembly.GetExecutingAssembly());
        }

Next, let’s see what controllers look like:

namespace CQRSAndMediator.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class BookController : ControllerBase
    {
        private readonly IMediator _mediator;
        public BookController(IMediator mediator)
        {
            _mediator = mediator;
        }

        [HttpPost]
        public IActionResult PostBook([FromBody] CreateBookRequestModel requestModel)
        {
            var response = _mediator.Send(requestModel);
            return Ok(response);
        }

        [HttpGet()]
        public IActionResult GetBookById([FromQuery] GetBookByIdRequestModel requestModel)
        {
            var response = _mediator.Send(requestModel);
            return Ok(response);
        }
    }
}

As you can see, we only need MediatR interface in controllers. This is why people often call MediatR a “fat controller diet”. MeadiatR has a Send method that receives a so-called request model. Based on the request model, MediatR is looking for an appropriate handler (service) that will return the right response.

Now we will follow through the GET request.

namespace CQRSAndMediator.Handlers.QueryHandlers
{
    public class GetBookByIdQueryHandler : IRequestHandler<GetBookByIdRequestModel, GetBookByIdResponseModel>
    {
        public async Task<GetBookByIdResponseModel> Handle(GetBookByIdRequestModel request, CancellationToken cancellationToken)
        {
            var bookDetails = new GetBookByIdResponseModel();
            // Business logic goes here
            // Read from ReadDB
            return bookDetails;
        }
    }
}

When creating a request handler, we need to inherit the IRequestHandler interface (part of MediatR), and there we will identify which request matches which response. We only need to implement Handle method that will contain the entire business logic to obtain the requested data.

This way, our handlers will have only one responsibility. In case we need to make any changes to our API we will be certain that it won’t impact any other services or flows (this is something that often happens when we are reusing methods). This way of separation of concerns will give us easily testable handlers.

When it comes to request objects, those are C# classes that have to inherit IRequest interface in order for MediatR to recognize them as request models:

namespace CQRSAndMediator.RequestModels.QueryRequestModels
{
    public class GetBookByIdRequestModel : IRequest<GetBookByIdResponseModel>
    {
        public long BookId { get; set; }
    }
}

Responses are just basic C# classes:

namespace CQRSAndMediator.ResponseModels.QueryResponseModels
{
    public class GetBookByIdResponseModel
    {
        public long BookId { get; set; }
        public string BookName { get; set; }
        public int Quantity { get; set; }
        public double Amount { get; set; }
    }
}

The POST request goes through command flow and the only difference is that we are saving or updating data in write database:

namespace CQRSAndMediator.Handlers.CommandHandlers
{
    public class CreateBookCommandHandler : IRequestHandler<CreateBookRequestModel, CreateBookResponseModel>
    {
        public async Task<CreateBookResponseModel> Handle(CreateBookRequestModel request, CancellationToken cancellationToken)
        {
            var result = new CreateBookResponseModel
            {
                IsSuccess = true,
            };

            // Your business logic here
            // Insert into the write DB
            return result;
        }
    }
}

 Now let’s take a look at the folder structure:

Organizing files this way was most intuitive for me. You can also add sub-folders for each entity so you can find the desired handler easier.

 Conclusion

Before you start using CQRS and MediatR (or any other library, code pattern, architectural pattern, etc.) make sure that you do thorough research about how it works. Only then you can determine if this combo is the right fit for your use case.

CQRS and MediatR are good together when you need an easy testable and performant application. A simple way of implementing them is by using MongoDB for a read database so you have your data in JSON format ready for the client. Meanwhile, you can use any kind of database for write purposes.

Bonus: This combo enhances database security.

Tags: , , ,

Leave a Reply

Your email address will not be published.

After you leave a comment, it will be held for moderation, and published afterwards.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.