Embarking on a journey into the realm of design patterns, let’s delve into the intricacies of the CQRS (Command Query Responsibility Segregation) pattern and its implementation in the .NET Core framework.
Understanding CQRS: A Paradigm Shift in Design
CQRS, an architectural pattern, introduces a fundamental shift in the way we design and structure our applications. By segregating the responsibilities of handling commands (write operations) and queries (read operations), CQRS opens the door to enhanced scalability, maintainability, and flexibility. We’ll explore the anatomy of the pattern, its key components, and how it decouples the command and query pathways.
This separation allows you to independently scale and optimize your read and write workloads and offers a high level of flexibility.
The CQRS pattern is based on the principle that the data model used to update an application can be different from the model used to read data from it. This separation provides several benefits:
Performance Optimization: By separating reads and writes, you can optimize each operation independently.
Complexity Management: CQRS allows you to handle complex business logic more effectively by focusing on one task at a time.
Scalability: With CQRS, you can scale your read and write operations independently, which is particularly useful in cloud-based applications.
However, implementing CQRS can also introduce some challenges:
Increased Complexity: CQRS can add complexity to your application, especially if your read and write operations are not significantly different.
Data Consistency: Since reads and writes are handled separately, there can be a delay in reflecting the updates in the read model.
Implementing CQRS in .NET Core: Bringing Theory to Reality
With the powerful ecosystem of .NET Core, implementing CQRS becomes an exciting endeavor. We’ll walk through practical examples, showcasing how to structure your application using separate models for commands and queries. Learn about the role of command handlers, query handlers, and the intricate dance between them. Witness the elegance of CQRS in action as we build a sample application step by step.
Here’s a step-by-step guide on how to implement the CQRS pattern in a .NET Core application:
Define Your Commands and Queries: Commands are methods that change the state of your data, while queries are methods that read your data.
Create Command and Query Handlers: These handlers contain the logic for handling commands and queries.
Use MediatR Library: MediatR is a popular library in .NET Core for implementing the CQRS pattern. It provides a simple, unambitious mediator implementation in .NET.
Implement Command and Query Handlers: With MediatR, each command or query has its own handler. This separation makes it easier to maintain and test your application.
I have created a Microservice application in .Net core (using .Net 6) which follows the Clean Architecture. Below is the folder structure .
The Ordering API service (/checkoutorder) is responsible for checking out the order request.
Under the CQRS pattern notion, queries and commands are developed individually; if we need to acquire data from the database, we use Queries; if we need to change or enter data into the database, we use Commands.
Queries :
The Handler class is found in the GetOrderList subdirectory. When we invoke it through the mediator, it connects to the DB repository interface to retrieve the OrderList.
//class : GetOrdersListQuery
using System;
using MediatR;
namespace Ordering.Application.Features.Orders.Queries.GetOrdersList
{
public class GetOrdersListQuery : IRequest<List<OrdersVm>>
{
public string UserName { get; set; }
public GetOrdersListQuery(string userName)
{
UserName = userName ?? throw new ArgumentNullException(nameof(userName));
}
}
}
//class : GetOrdersListQueryHandler
using System;
using AutoMapper;
using MediatR;
using Ordering.Application.Contracts.Persistence;
namespace Ordering.Application.Features.Orders.Queries.GetOrdersList
{
public class GetOrdersListQueryHandler : IRequestHandler<GetOrdersListQuery, List<OrdersVm>>
{
private readonly IOrderRepository _orderRepository;
private readonly IMapper _mapper;
public GetOrdersListQueryHandler(IOrderRepository orderRepository, IMapper mapper)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
public async Task<List<OrdersVm>> Handle(GetOrdersListQuery request,
CancellationToken cancellationToken)
{
var orderList = await _orderRepository.GetOrdersByUserName(request.UserName);
return _mapper.Map<List<OrdersVm>>(orderList);
}
}
}
Commands:
For the checkout order command, we are inserting the new order entity into the database. Before inserting this request into the database, we can validate the parameters of the request using Fluent validation, as shown below. If the validation is successful, the handler will proceed with the insertion using the AddAsync() method.
using System;
using FluentValidation;
namespace Ordering.Application.Features.Orders.Commands.CheckoutOrder
{
public class CheckoutOrderCommandValidator : AbstractValidator<CheckoutOrderCommand>
{
public CheckoutOrderCommandValidator()
{
RuleFor(p => p.UserName)
.NotEmpty().WithMessage("{UserName} is required.")
.NotNull()
.MaximumLength(50).WithMessage("{UserName} must not exceed 50 characters.");
RuleFor(p => p.EmailAddress)
.NotEmpty().WithMessage("{EmailAddress} is required.");
RuleFor(p => p.TotalPrice)
.NotEmpty().WithMessage("{TotalPrice} is required.")
.GreaterThan(0).WithMessage("{TotalPrice} should be greater than zero.");
}
}
}
// Hanlder class :
using System;
using AutoMapper;
using MediatR;
using Microsoft.Extensions.Logging;
using Ordering.Application.Contracts.Infrastructure;
using Ordering.Application.Contracts.Persistence;
using Ordering.Application.Models;
using Ordering.Domain.Entities;
namespace Ordering.Application.Features.Orders.Commands.CheckoutOrder
{
public class CheckoutOrderCommandHandler : IRequestHandler<CheckoutOrderCommand, int>
{
private readonly IOrderRepository _orderRepository;
private readonly IMapper _mapper;
private readonly IEmailService _emailService;
private readonly ILogger<CheckoutOrderCommandHandler> _logger;
public CheckoutOrderCommandHandler(IOrderRepository orderRepository, IMapper mapper, IEmailService emailService, ILogger<CheckoutOrderCommandHandler> logger)
{
_orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<int> Handle(CheckoutOrderCommand request, CancellationToken cancellationToken)
{
var orderEntity = _mapper.Map<Order>(request);
var newOrder = await _orderRepository.AddAsync(orderEntity);
_logger.LogInformation($"Order {newOrder.Id} is successfully created.");
await SendMail(newOrder);
return newOrder.Id;
}
}
}
Below is the code snippet to call this handler from the controller.
// To get list
[HttpGet("{userName}", Name = "GetOrder")]
[ProducesResponseType(typeof(IEnumerable<OrdersVm>), (int)HttpStatusCode.OK)]
public async Task<ActionResult<IEnumerable<OrdersVm>>> GetOrdersByUserName(string userName)
{
var query = new GetOrdersListQuery(userName);
var orders = await _mediator.Send(query);
return Ok(orders);
}
// to insert into the DB
[HttpPost(Name = "CheckoutOrder")]
[ProducesResponseType((int)HttpStatusCode.OK)]
public async Task<ActionResult<int>> CheckoutOrder([FromBody] CheckoutOrderCommand command)
{
var result = await _mediator.Send(command);
return Ok(result);
}
In the realm of modern application development, CQRS stands as a beacon of innovation. “Mastering CQRS Pattern in .NET Core” empowers you to harness its potential, providing a comprehensive guide to implementing, understanding, and mastering this transformative pattern.