Using CQRS pattern with MediatR in .Net Core

Context and problem

In traditional architectures, the same data model is used to query and update a database. That’s simple and works well for basic CRUD operations. In more complex applications, however, this approach can become unwieldy. For example, on the read side, the application may perform many different queries, returning data transfer objects (DTOs) with different shapes. Object mapping can become complicated. On the write side, the model may implement complex validation and business logic. As a result, you can end up with an overly complex model that does too much.

Read and write workloads are often asymmetrical, with very different performance and scale requirements.

  • There is often a mismatch between the read and write representations of the data, such as additional columns or properties that must be updated correctly even though they aren’t required as part of an operation.
  • Data contention can occur when operations are performed in parallel on the same set of data.
  • The traditional approach can have a negative effect on performance due to the load on the data store and data access layer, and the complexity of queries required to retrieve information.
  • Managing security and permissions can become complex because each entity is subject to both read and write operations, which might expose data in the wrong context.

Solution

CQRS addresses separate reads and writes into separate models, using commands to update data, and queries to read data.

  • Commands should be task-based, rather than data-centric. (“Book hotel room,” not “set ReservationStatus to Reserved.”) Commands may be placed on a queue for asynchronous processing, rather than being processed synchronously.
  • Queries never modify the database. A query returns a DTO that does not encapsulate any domain knowledge.

The models can then be isolated, as shown in the following diagram, although that’s not an absolute requirement.

Source from: Microsoft — Command and Query Responsibility Segregation (CQRS) pattern

MediatR is an open source implementation of the mediator pattern that doesn’t try to do too much and performs no magic. It allows you to compose messages, create and listen for events using synchronous or asynchronous patterns. It helps to reduce coupling and isolate the concerns of requesting the work to be done and creating the handler that dispatches the work.

Thin Controllers

  • In the traditional Controllers, you usually implement almost business logic flow in like as Validation, Mapping Objects, Savings Object, or Get Object, Return HTTP status code of request and the data response if have. However, if you will loop almost these steps again in every controller, your controller will get more fat. And who will maintain these controllers like this, I believe you will want to throw it away because too hard to see, let’s see these pseudo codes:
[AllowAnonymous]
[HttpPost]
public async Task<IHttpActionResult> Register (RegisterBindingModel model) {
    if (!ModelState.IsValid) return BadRequest (ModelState);

    var user = new User {
        UserName = model.Email,
        Email = model.Email,
        DateOfBirth = DateTime.Parse (model.DateOfBirth.ToString (CultureInfo.InvariantCulture)),
        PhoneNumber = model.PhoneNumber,
        Name = model.Name
    };

    var result = await UserManager.CreateAsync (user, model.Password);

    if (!result.Succeeded) return GetErrorResult (result);

    return Ok ();
}

We can replace by these simple pseudo code like this:

[AllowAnonymous]
[HttpPost]
public async Task<IHttpActionResult> Register ([FromBody] CreateUserCommand command) {
    return Ok (await new CommandAsync (command));
}

CQS

This approach allows separate read and write operations. But for commands and queries, we need some handlers. And this is exactly where we can apply MediatR library.

As I mentioned before, this library is a simple implementation of MediatR pattern. This pattern allows implementing commands/queries and handlers loosely coupled.

Example MediatR

For example, I would like to implement code to get a user’s detail

  • Request

When you start using MediatR library, the first thing you need to define is request. Requests describe your commands and queries behavior.

public class GetUserDetailQuery : IRequest<UserDto>
{
    public GetUserDetailQuery(int id) {
    	Id = id;
    }
    
    public int Id {get;}
}

The GetUserDetailQuery class describes a query that requires Id and returns the User data transfer object. All requests should implement IRequest interface. This interface is a contract which will be used by the handler to process request.

  • Handler:

When a request is created, you will need a handler to solve your request:

public class GetUserDetailHandler : IRequestHandler<GetUserDetailQuery, UserDto> 
{
   	private readonly IUserRepository _userRepository;
    private readonly IUserMapper _userMapper;
    
    public GetUserDetailHandler(IUserRepository userRepository, IUserMapper userMapper) {
    	_userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
    	_userMapper = userMapper ?? throw new ArgumentNullException(nameof(userMapper));
    }
    
    public async Task<UserDto> Handle(GetUserDetailQuery request, CancellationToken cancellationToken) {
    var user = await _userRepository.GetAsync(u => u.Id == request.Id)
    
    if user == null {
    	return null
    }
    
    var userDto = _userMapper.MapUserDto(user);
    return userDto;
    }
}

All handlers should implement IRequestHandler interface. This interface depends on two parameters. First — requestsecond — response. More details about requests and handlers you can find here.

  • Controller

So, we have our query and handler, but how to use them?

public class UsersController : ControllerBase 
{
	private readonly IMediator _mediator;

	public UsersController(IMediator mediator) {
		_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
	}

	[HttpGet("{id}")]
	public async Task<Order> Get(int id) {
    	var user = await _mediator.Send(new GetUserDetailQuery(id));
        if user == null {
        	return NotFound();
        }
        return Ok(user);
    }
}

Here are some instances where it may not be a good idea to build a system using CQRS:

  • User interfaces with simple CRUD operations. If the system you are designing is only an implementation of CRUD operations, CQRS will add excessive complexity to its implementation. At the same time, you won’t benefit from the main advantages of this pattern.
  • You use simple business logic. If the process you need to implement is simple and each operation has the same number of reads and writes, you probably don’t need to use CQRS. You won’t need to scale read and write operations separately or add unnecessary complexity to the implementation.
  • If you don’t need to scale the system. Similar to the previous point, if you are sure of the load that your system will have over time, and that the system you are designing doesn’t need to be scaled, you probably don’t need to use CQRS.

This can be very difficult to predict. The needs and usage of a system usually changes over time and, although at the beginning of the project you may not expect a large workload, this may change over time and become an issue. Even so, if you’re sure that you won’t need to scale your system, you should take this into account when deciding to use CQRS or not.

  • Apply CQRS to the whole system. As mentioned earlier, CQRS doesn’t need to be used in all parts of the system. It is important to define your Bounded Contexts and identify in which of them would benefit most from the use of CQRS.

If your system isn’t divided into small components with a limited and well defined responsibility, it’s not a good idea to use CQRS. The first step should be to divide your system into small and manageable parts. Then consider which of these components would work better using CQRS.

  • Eventual consistency. One of the main characteristics of CQRS system is that it always has an “eventual consistency” in the data. That is, the information that the query subsystem retrieves may not always be updated with the latest version that has been processed. This is due to the synchronization process that updates the information in the query subsystem repository. When processing the events, it can cause a certain lag between the executed operations and the information retrieved in the queries. If you use CQRS you should be aware of this eventual consistency and, in the case of your  business needing to have strong consistency, you should not use CQRS.
SiteLock