HardПрактика2 min

Шаг 9: Commands и Handlers (CQRS)

Создание CreateOrderCommand, ConfirmOrderCommand, CancelOrderCommand с Handlers: паттерн CQRS и работа MediatR

Шаг 9: Commands и Handlers

CQRS разделяет операции: Command изменяет данные, Query читает. MediatR связывает Command с Handler автоматически.

Структура Application Layer

Создайте папки в OrderManagement.Application:

  • Common/Interfaces, Common/Behaviors
  • Orders/Commands/CreateOrder, Orders/Commands/ConfirmOrder, Orders/Commands/CancelOrder
  • Orders/Queries/GetOrder, Orders/Queries/GetCustomerOrders
  • Orders/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 без параметра -- команда ничего не возвращает. ?? выбрасывает исключение, если заказ не найден.

Проверь себя

Как MediatR находит нужный Handler для команды?

Что делает structured logging в _logger.LogInformation?

Зачем Handler использует IOrderRepository, а не OrderDbContext напрямую?