Шаг 9: Commands и Handlers
CQRS разделяет операции: Command изменяет данные, Query читает. MediatR связывает Command с Handler автоматически.
Структура Application Layer
Создайте папки в OrderManagement.Application:
Common/Interfaces,Common/BehaviorsOrders/Commands/CreateOrder,Orders/Commands/ConfirmOrder,Orders/Commands/CancelOrderOrders/Queries/GetOrder,Orders/Queries/GetCustomerOrdersOrders/DTOs
DTOs
В Orders/DTOs/OrderDto.cs:
namespace OrderManagement.Application.Orders.DTOs;
public record OrderDto(
Guid Id, string CustomerId, string Status,
string ShippingAddress, decimal TotalAmount,
string Currency, DateTime CreatedAt,
List<OrderItemDto> Items);
public record OrderItemDto(
Guid Id, string ProductId, string ProductName,
decimal UnitPrice, int Quantity, decimal TotalPrice);
CreateOrderCommand
using MediatR;
namespace OrderManagement.Application.Orders.Commands.CreateOrder;
public record CreateOrderCommand(
string CustomerId, string Street, string City,
string PostalCode, string Country,
List<CreateOrderItemDto> Items) : IRequest<Guid>;
public record CreateOrderItemDto(
string ProductId, string ProductName,
decimal UnitPrice, string Currency, int Quantity);
IRequest<Guid> сообщает MediatR: для этой команды нужен Handler, возвращающий Guid.
CreateOrderCommandHandler
using MediatR;
using Microsoft.Extensions.Logging;
using OrderManagement.Domain.Entities;
using OrderManagement.Domain.Interfaces;
using OrderManagement.Domain.ValueObjects;
namespace OrderManagement.Application.Orders.Commands.CreateOrder;
public class CreateOrderCommandHandler
: IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _orderRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<CreateOrderCommandHandler> _logger;
public CreateOrderCommandHandler(
IOrderRepository orderRepository,
IUnitOfWork unitOfWork,
ILogger<CreateOrderCommandHandler> logger)
{
_orderRepository = orderRepository;
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task<Guid> Handle(
CreateOrderCommand command, CancellationToken ct)
{
var address = new ShippingAddress(
command.Street, command.City,
command.PostalCode, command.Country);
var order = Order.Create(command.CustomerId, address);
foreach (var item in command.Items)
{
order.AddItem(item.ProductId, item.ProductName,
new Money(item.UnitPrice, item.Currency),
item.Quantity);
}
await _orderRepository.AddAsync(order, ct);
await _unitOfWork.SaveChangesAsync(ct);
_logger.LogInformation(
"Order {OrderId} created for {CustomerId}",
order.Id, command.CustomerId);
return order.Id;
}
}
DI подставляет реализации автоматически. AddAsync помечает объект для вставки, SaveChangesAsync выполняет SQL.
ConfirmOrderCommand
public record ConfirmOrderCommand(Guid OrderId) : IRequest;
public class ConfirmOrderCommandHandler
: IRequestHandler<ConfirmOrderCommand>
{
private readonly IOrderRepository _orderRepository;
private readonly IUnitOfWork _unitOfWork;
public ConfirmOrderCommandHandler(
IOrderRepository orderRepository, IUnitOfWork unitOfWork)
{ _orderRepository = orderRepository; _unitOfWork = unitOfWork; }
public async Task Handle(
ConfirmOrderCommand command, CancellationToken ct)
{
var order = await _orderRepository
.GetByIdAsync(command.OrderId, ct)
?? throw new OrderNotFoundException(command.OrderId);
order.Confirm();
_orderRepository.Update(order);
await _unitOfWork.SaveChangesAsync(ct);
}
}
IRequest без параметра -- команда ничего не возвращает. ?? выбрасывает исключение, если заказ не найден.