In the previous post, we discussed how to build a web API applying useful patterns like dependency injection, repository, and some NuGet packages like automapper Part 1.
At this time, we are continuing with CQRS and Mediator patterns.
Before starting with visual studio, we explain what kind of problems resolve CQRS and Mediator patterns.
CQRS
CQRS are the acronyms of Command Query and Responsibility Segregation. This pattern pursues how to separate read and update operations for CRUD. However, be careful not to apply for any project and add extra complexity because this pattern is not bulletproof.
Why use CQRS
In common architectures, the typical case is to use the same model for query and update databases, which works perfectly for simple CRUD operations. But in complex projects, it can be tricky. To be more explicit, we would need to perform different queries returning specific Dtos, and the write models set multiples and complex validations following the business logic.
Can you check more about CQRS
Mediator Pattern
This pattern is part of the family of the behavior design pattern, and the main goal is coordinate the relations between associate objects. And is help us to define how to encapsulate the integration of each of these different objects. On the other hand, it Promotes low coupling to prevent these objects from being referenced explicitly and allows to change dependency easier between them.
Why use Mediator
In this case, we need to use this pattern because we are creating a mechanism to communicate all requests to use queries or commands through the handlers.
The handler is the class where you create the task, follow the queries or commands requirements, accessing the storage through repositories.
MediatR
But to facilitate the implementation of these patterns described before, we are using these NuGet packages.
*MediatR
*MediatR.Extensions.Microsoft.DependencyInjection
Before to start creating commands and query models, we are adding the service MediatR to the program.cs file like this way :
builder.Services.AddMediatR(Assembly.GetCallingAssembly();
Step 1
Now We have to build different types of elements for the queries, commands, and handlers. Therefore we have to add these folders to keep a clean structure.
/queries
/commands
/handlers
The next step is to start defining the queries and command models.
Tips:
Commands: should be defined like a simple name task and not describe action data on the database.
Example “CreateNewCustomer” instead of “InsertCustomer”
Queries: the queries never update the database. Only could return dtos with the data requested.
Step 2
Queries
Now Let’s go to create the query models.
In this case, we are creating the query models, so we will be required to use the IRequest interface with the dto to return.
In this case, we take advantage of records. In this way, we don't have to define a constructor and keep the immutability.
GetAllCustomersQuery
using MediatR;
using WebApiCustomers.Dtos;
namespace WebApiCustomers.Queries;
public sealed record GetAllCustomersQuery : IRequest<List<CustomerReadDto>>;
GetOneCustomerByIdentifierQuery
using MediatR;
using WebApiCustomers.Dtos;
namespace WebApiCustomers.Queries;
public sealed record GetOneCustomerByIdentifierQuery (int Id) :IRequest<CustomerReadDto>;
Commands
In this step, we continue creating the commands.
CreateNewCustomerCommand
In this case, we are creating a simple record that returns the CustomerCreateDto object.
using MediatR;
using WebApiCustomers.Dtos;
namespace WebApiCustomers.Commands;
public sealed record CreateNewCustomerCommand (CustomerCreateDto customerToCreate) : IRequest<CustomerReadDto>;
EditExistingCustomerCommand
In this case, the IRequest interface has no parameters because we don't want to return anything.
using MediatR;
using WebApiCustomers.Dtos;
namespace WebApiCustomers.Commands;
public sealed record EditExistingCustomerCommand (CustomerUpdateDto customerToUpdate) : IRequest;
RemoveExistingCustomer
We have the same signature in this command because we don't want to return anything either.
using MediatR;
using WebApiCustomers.Dtos;
namespace WebApiCustomers.Commands;
public sealed record RemoveExistingCustomerCommand (int Id) : IRequest;
Step 3
Now we are ready to add our handlers for queries and command models.
Handlers
The handlers are responsible for receiving the request and encapsulating the functionality to accomplish the incoming request with specific signatures that may change if they are queries or command requests inherited from the IRequestHandler interface.
Now we are defining these handlers.
GetAllCustomersHandler
On this handler, we are using the repository to get all customers and mapping the object to the requested object CustomerReadDto
using AutoMapper;
using MediatR;
using WebApiCustomers.Dtos;
using WebApiCustomers.Queries;
using WebApiCustomers.Repositories;
namespace WebApiCustomers.Handlers;
public class GetAllCustomersHandler : IRequestHandler<GetAllCustomersQuery, List<CustomerReadDto>>
{
private readonly ICustomerRepository _customerRepository;
private IMapper _mapper;
public GetAllCustomersHandler(ICustomerRepository customerRepository, IMapper mapper)
{
_customerRepository = customerRepository;
_mapper = mapper;
}
public async Task<List<CustomerReadDto>> Handle(GetAllCustomersQuery request, CancellationToken cancellationToken)
{
var customerList = await _customerRepository.GetAllAsync();
return _mapper.Map<List<CustomerReadDto>>(customerList);
}
}
GetOneCustomerByIdentifierHandler
On this handler, we are using the repository to get one customer by identifier and mapping the requested object CustomerReadDto.
Using AutoMapper;
using MediatR;
using WebApiCustomers.Data;
using WebApiCustomers.Dtos;
using WebApiCustomers.Queries;
using WebApiCustomers.Repositories;
namespace WebApiCustomers.Handlers;
public sealed class GetOneCustomerByIdentifierHandler :
IRequestHandler<GetOneCustomerByIdentifierQuery, CustomerReadDto>
{
private readonly ICustomerRepository _customerRepository;
private readonly IMapper _mapper;
public GetOneCustomerByIdentifierHandler(ICustomerRepository customerRepository, IMapper mapper )
{
_customerRepository = customerRepository;
_mapper = mapper;
}
public async Task<CustomerReadDto> Handle(GetOneCustomerByIdentifierQuery request, CancellationToken cancellationToken)
{
var customer = await _customerRepository.GetAsync(request.Id);
return _mapper.Map<CustomerReadDto>(customer);
}
}
CreateNewCustomerHandler
On this handler, we are using the repository to create a new customer and mapping the object requested object CustomerReadDto.
using AutoMapper;
using MediatR;
using WebApiCustomers.Commands;
using WebApiCustomers.Data;
using WebApiCustomers.Dtos;
using WebApiCustomers.Repositories;
namespace WebApiCustomers.Handlers;
public class CreateNewCustomerHandler : IRequestHandler<CreateNewCustomerCommand, CustomerReadDto>
{
private readonly ICustomerRepository _customerRepository;
private readonly IMapper _mapper;
public CreateNewCustomerHandler(ICustomerRepository customerRepository, IMapper mapper)
{
_customerRepository = customerRepository;
_mapper = mapper;
}
public async Task<CustomerReadDto?> Handle(
CreateNewCustomerCommand request,
CancellationToken cancellationToken)
{
var customerToInsert = _mapper.Map<Customer>(request.customerToCreate);
await _customerRepository.AddAsync(customerToInsert);
await _customerRepository.SaveAsync();
var result = _mapper.Map<CustomerReadDto>(customerToInsert);
return result == null ? null : await Task.FromResult(result);
}
}
EditExistingCustomerHandler
On this handler, we are using the repository to edit the requested and return the Unit object because we don't need to return it.
using AutoMapper;
using MediatR;
using WebApiCustomers.Commands;
using WebApiCustomers.Data;
using WebApiCustomers.Dtos;
using WebApiCustomers.Repositories;
namespace WebApiCustomers.Handlers;
public sealed class EditExistingCustomerHandler : IRequestHandler<EditExistingCustomerCommand, Unit>
{
private readonly ICustomerRepository _customerRepository;
private readonly IMapper _mapper;
public EditExistingCustomerHandler(ICustomerRepository customerRepository,
IMapper mapper )
{
_customerRepository = customerRepository;
_mapper = mapper;
}
public async Task<Unit> Handle(EditExistingCustomerCommand request, CancellationToken cancellationToken)
{
var customerToUpdate = _mapper.Map<Customer>(request.customerToUpdate);
await _customerRepository.UpdateAsync(customerToUpdate);
return Unit.Value;
}
}
RemoveExistingCustomerHandler
On this handler, we are using the repository to remove the requested customer and return the Unit object when we don't need to return it.
using AutoMapper;
using MediatR;
using WebApiCustomers.Commands;
using WebApiCustomers.Repositories;
namespace WebApiCustomers.Handlers;
public sealed class RemoveExistingCustomerHandler : IRequestHandler<RemoveExistingCustomerCommand, Unit>
{
private readonly ICustomerRepository _customerRepository;
private readonly IMapper _mapper;
public RemoveExistingCustomerHandler(ICustomerRepository customerRepository,
IMapper mapper)
{
_customerRepository = customerRepository;
_mapper = mapper;
}
public async Task<Unit> Handle(RemoveExistingCustomerCommand request, CancellationToken cancellationToken)
{
await _customerRepository.DeleteAsync(request.Id);
await _customerRepository.SaveAsync();
return Unit.Value;
}
}
Controller
In the last step, we update the controller class to remove unnecessary injected objects and use the method “Send” from the mediator object.
using AutoMapper;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WebApiCustomers.Commands;
using WebApiCustomers.Data;
using WebApiCustomers.Dtos;
using WebApiCustomers.Queries;
using WebApiCustomers.Repositories;
namespace WebApiCustomers.Controllers;
[Route("api/[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
private readonly IMediator _mediator;
public CustomersController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var result = await _mediator.Send( new GetAllCustomersQuery());
return Ok(result);
}
[HttpGet("{id:int}")]
public async Task<IActionResult> Get(int id)
{
var result = await _mediator.Send(new GetOneCustomerByIdentifierQuery(id));
return result == null ? NotFound() : Ok(result);
}
[HttpPost]
public async Task<IActionResult> PostCustomer([FromBody] CustomerCreateDto customerCreateDto)
{
var result = await _mediator.Send(
new CreateNewCustomerCommand(customerCreateDto));
return CreatedAtAction("Get", new { id = result.Id }, result);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateCustomer(int id, [FromBody] CustomerUpdateDto customerUpdateDto)
{
if (id != customerUpdateDto.Id) return BadRequest();
await _mediator.Send(new EditExistingCustomerCommand(customerUpdateDto));
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteCustomer(int id)
{
await _mediator.Send(new RemoveExistingCustomerCommand(id));
return NoContent();
}
}
Recaps
On these topics, we gain new tools to reduce the complexity of big projects and how we can apply different queries and command models.
Secondly, we learned how to write more readable code, reducing the responsibility from the controllers and encapsulating the logic, and creating coupled requests.
In the end, with learned about these topics:
- CQRS and Mediator Patterns.
- How to separate Queries and Commands Models
- How to implement the MediatR Nuget Package.
Source code WebApiCustomers Part 2
If you enjoyed this article, please subscribe and follow this series about building clean web API Net Core 6.
Top comments (2)
Thank you sir I learn alot! :D
Btw do you have a source code that support mediatR v12
Great @ljpogi I'm glad that you find it useful post, at the moment I don't have it , but I work on that.
Let me know other topics you would like it.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.