← Назад к вопросам

CQRS: Разделение команд и запросов

3.0 Senior🔥 131 комментариев
#Dependency Injection и IoC#Архитектура и микросервисы#ООП и паттерны проектирования

Условие

Реализуйте простую CQRS-архитектуру для сервиса управления продуктами.

Требования:

  1. Разделить операции чтения (Query) и записи (Command)
  2. Создать отдельные модели для чтения и записи
  3. Реализовать базовые интерфейсы ICommand, IQuery, ICommandHandler, IQueryHandler

Пример структуры:

// Commands public record CreateProductCommand(string Name, decimal Price) : ICommand<int>; public record UpdateProductCommand(int Id, string Name, decimal Price) : ICommand;

// Queries public record GetProductByIdQuery(int Id) : IQuery<ProductDto>; public record GetAllProductsQuery() : IQuery<IEnumerable<ProductDto>>;

// Handlers public class CreateProductHandler : ICommandHandler<CreateProductCommand, int> { // TODO: реализовать }

Критерии оценки:

  • Понимание CQRS паттерна
  • Корректное разделение ответственностей
  • Использование Dependency Injection

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

CQRS: Разделение команд и запросов

Что такое CQRS?

CQRS (Command Query Responsibility Segregation) — это паттерн, который разделяет операции на две части:

  • Commands — операции, которые изменяют состояние (Create, Update, Delete)
  • Queries — операции, которые только читают данные (Get, List)

Это позволяет:

  • Оптимизировать чтение и запись отдельно
  • Использовать разные модели данных
  • Масштабировать независимо
  • Улучшить производительность

Базовые интерфейсы

// Маркер для команд
public interface ICommand { }

// Команда с результатом
public interface ICommand<out TResult> { }

// Обработчик команды без результата
public interface ICommandHandler<in TCommand> 
    where TCommand : ICommand
{
    Task HandleAsync(TCommand command);
}

// Обработчик команды с результатом
public interface ICommandHandler<in TCommand, TResult> 
    where TCommand : ICommand<TResult>
{
    Task<TResult> HandleAsync(TCommand command);
}

// Маркер для запросов
public interface IQuery<out TResult> { }

// Обработчик запроса
public interface IQueryHandler<in TQuery, TResult> 
    where TQuery : IQuery<TResult>
{
    Task<TResult> HandleAsync(TQuery query);
}

// Dispatcher для отправки команд и запросов
public interface ICommandDispatcher
{
    Task<TResult> DispatchAsync<TResult>(ICommand<TResult> command);
    Task DispatchAsync(ICommand command);
}

public interface IQueryDispatcher
{
    Task<TResult> DispatchAsync<TQuery, TResult>(TQuery query) 
        where TQuery : IQuery<TResult>;
}

Commands и Handlers

// ======== КОМАНДЫ ========

// Создание продукта с возвращением ID
public record CreateProductCommand(
    string Name,
    string Description,
    decimal Price,
    int StockQuantity
) : ICommand<int>;

// Обновление продукта без результата
public record UpdateProductCommand(
    int Id,
    string Name,
    string Description,
    decimal Price,
    int StockQuantity
) : ICommand;

// Удаление продукта
public record DeleteProductCommand(int Id) : ICommand;

// ======== ОБРАБОТЧИКИ КОМАНД ========

public class CreateProductHandler : ICommandHandler<CreateProductCommand, int>
{
    private readonly IProductRepository _repository;
    private readonly IProductValidator _validator;
    private readonly IEventPublisher _eventPublisher;
    
    public CreateProductHandler(
        IProductRepository repository,
        IProductValidator validator,
        IEventPublisher eventPublisher)
    {
        _repository = repository;
        _validator = validator;
        _eventPublisher = eventPublisher;
    }
    
    public async Task<int> HandleAsync(CreateProductCommand command)
    {
        // 1. Валидация
        var validationResult = _validator.ValidateCreate(command);
        if (!validationResult.IsValid)
            throw new ValidationException(validationResult.Errors);
        
        // 2. Создание сущности
        var product = new Product
        {
            Name = command.Name,
            Description = command.Description,
            Price = command.Price,
            StockQuantity = command.StockQuantity,
            CreatedAt = DateTime.UtcNow
        };
        
        // 3. Сохранение в БД
        var productId = await _repository.CreateAsync(product);
        
        // 4. Публикация события
        await _eventPublisher.PublishAsync(
            new ProductCreatedEvent(productId, command.Name));
        
        return productId;
    }
}

public class UpdateProductHandler : ICommandHandler<UpdateProductCommand>
{
    private readonly IProductRepository _repository;
    private readonly IProductValidator _validator;
    private readonly IEventPublisher _eventPublisher;
    
    public UpdateProductHandler(
        IProductRepository repository,
        IProductValidator validator,
        IEventPublisher eventPublisher)
    {
        _repository = repository;
        _validator = validator;
        _eventPublisher = eventPublisher;
    }
    
    public async Task HandleAsync(UpdateProductCommand command)
    {
        // 1. Получить существующий продукт
        var product = await _repository.GetByIdAsync(command.Id)
            ?? throw new NotFoundException($"Product {command.Id} not found");
        
        // 2. Валидация
        var validationResult = _validator.ValidateUpdate(command);
        if (!validationResult.IsValid)
            throw new ValidationException(validationResult.Errors);
        
        // 3. Обновление сущности
        product.Name = command.Name;
        product.Description = command.Description;
        product.Price = command.Price;
        product.StockQuantity = command.StockQuantity;
        product.UpdatedAt = DateTime.UtcNow;
        
        // 4. Сохранение
        await _repository.UpdateAsync(product);
        
        // 5. Публикация события
        await _eventPublisher.PublishAsync(
            new ProductUpdatedEvent(command.Id, command.Name));
    }
}

public class DeleteProductHandler : ICommandHandler<DeleteProductCommand>
{
    private readonly IProductRepository _repository;
    private readonly IEventPublisher _eventPublisher;
    
    public DeleteProductHandler(
        IProductRepository repository,
        IEventPublisher eventPublisher)
    {
        _repository = repository;
        _eventPublisher = eventPublisher;
    }
    
    public async Task HandleAsync(DeleteProductCommand command)
    {
        var product = await _repository.GetByIdAsync(command.Id)
            ?? throw new NotFoundException($"Product {command.Id} not found");
        
        await _repository.DeleteAsync(command.Id);
        
        await _eventPublisher.PublishAsync(
            new ProductDeletedEvent(command.Id, product.Name));
    }
}

Queries и Handlers

// ======== ЗАПРОСЫ ========

// DTO для чтения
public record ProductDto(
    int Id,
    string Name,
    string Description,
    decimal Price,
    int StockQuantity,
    DateTime CreatedAt
);

// Получить продукт по ID
public record GetProductByIdQuery(int Id) : IQuery<ProductDto>;

// Получить все продукты
public record GetAllProductsQuery(
    int PageNumber = 1,
    int PageSize = 10
) : IQuery<PaginatedResult<ProductDto>>;

// Поиск продуктов
public record SearchProductsQuery(
    string SearchTerm,
    decimal? MinPrice = null,
    decimal? MaxPrice = null
) : IQuery<IEnumerable<ProductDto>>;

// ======== ОБРАБОТЧИКИ ЗАПРОСОВ ========

public class GetProductByIdHandler : IQueryHandler<GetProductByIdQuery, ProductDto>
{
    private readonly IProductReadRepository _readRepository;
    
    public GetProductByIdHandler(IProductReadRepository readRepository)
    {
        _readRepository = readRepository;
    }
    
    public async Task<ProductDto> HandleAsync(GetProductByIdQuery query)
    {
        var product = await _readRepository.GetByIdAsync(query.Id)
            ?? throw new NotFoundException($"Product {query.Id} not found");
        
        return new ProductDto(
            product.Id,
            product.Name,
            product.Description,
            product.Price,
            product.StockQuantity,
            product.CreatedAt
        );
    }
}

public class GetAllProductsHandler : IQueryHandler<GetAllProductsQuery, PaginatedResult<ProductDto>>
{
    private readonly IProductReadRepository _readRepository;
    
    public GetAllProductsHandler(IProductReadRepository readRepository)
    {
        _readRepository = readRepository;
    }
    
    public async Task<PaginatedResult<ProductDto>> HandleAsync(GetAllProductsQuery query)
    {
        var (items, totalCount) = await _readRepository.GetPagedAsync(
            query.PageNumber,
            query.PageSize
        );
        
        var dtos = items.Select(p => new ProductDto(
            p.Id,
            p.Name,
            p.Description,
            p.Price,
            p.StockQuantity,
            p.CreatedAt
        )).ToList();
        
        return new PaginatedResult<ProductDto>
        {
            Items = dtos,
            TotalCount = totalCount,
            PageNumber = query.PageNumber,
            PageSize = query.PageSize
        };
    }
}

public class SearchProductsHandler : IQueryHandler<SearchProductsQuery, IEnumerable<ProductDto>>
{
    private readonly IProductReadRepository _readRepository;
    
    public SearchProductsHandler(IProductReadRepository readRepository)
    {
        _readRepository = readRepository;
    }
    
    public async Task<IEnumerable<ProductDto>> HandleAsync(SearchProductsQuery query)
    {
        var products = await _readRepository.SearchAsync(
            query.SearchTerm,
            query.MinPrice,
            query.MaxPrice
        );
        
        return products.Select(p => new ProductDto(
            p.Id,
            p.Name,
            p.Description,
            p.Price,
            p.StockQuantity,
            p.CreatedAt
        ));
    }
}

Dispatcher реализация

public class CommandDispatcher : ICommandDispatcher
{
    private readonly IServiceProvider _serviceProvider;
    
    public CommandDispatcher(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public async Task<TResult> DispatchAsync<TResult>(ICommand<TResult> command)
    {
        var handlerType = typeof(ICommandHandler<,>)
            .MakeGenericType(command.GetType(), typeof(TResult));
        
        var handler = _serviceProvider.GetService(handlerType)
            ?? throw new InvalidOperationException(
                $"No handler for command {command.GetType().Name}");
        
        var method = handlerType.GetMethod("HandleAsync");
        var result = await (Task<TResult>)method.Invoke(handler, new[] { command });
        
        return result;
    }
    
    public async Task DispatchAsync(ICommand command)
    {
        var handlerType = typeof(ICommandHandler<>)
            .MakeGenericType(command.GetType());
        
        var handler = _serviceProvider.GetService(handlerType)
            ?? throw new InvalidOperationException(
                $"No handler for command {command.GetType().Name}");
        
        var method = handlerType.GetMethod("HandleAsync");
        await (Task)method.Invoke(handler, new[] { command });
    }
}

public class QueryDispatcher : IQueryDispatcher
{
    private readonly IServiceProvider _serviceProvider;
    
    public QueryDispatcher(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public async Task<TResult> DispatchAsync<TQuery, TResult>(TQuery query)
        where TQuery : IQuery<TResult>
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(typeof(TQuery), typeof(TResult));
        
        var handler = _serviceProvider.GetService(handlerType)
            ?? throw new InvalidOperationException(
                $"No handler for query {typeof(TQuery).Name}");
        
        var method = handlerType.GetMethod("HandleAsync");
        var result = await (Task<TResult>)method.Invoke(handler, new[] { query });
        
        return result;
    }
}

Регистрация в DI контейнере

public static class CqrsExtensions
{
    public static IServiceCollection AddCqrs(this IServiceCollection services)
    {
        // Регистрируем dispatcher
        services.AddScoped<ICommandDispatcher, CommandDispatcher>();
        services.AddScoped<IQueryDispatcher, QueryDispatcher>();
        
        // Регистрируем обработчики команд
        services.AddScoped<ICommandHandler<CreateProductCommand, int>, CreateProductHandler>();
        services.AddScoped<ICommandHandler<UpdateProductCommand>, UpdateProductHandler>();
        services.AddScoped<ICommandHandler<DeleteProductCommand>, DeleteProductHandler>();
        
        // Регистрируем обработчики запросов
        services.AddScoped<IQueryHandler<GetProductByIdQuery, ProductDto>, GetProductByIdHandler>();
        services.AddScoped<IQueryHandler<GetAllProductsQuery, PaginatedResult<ProductDto>>, GetAllProductsHandler>();
        services.AddScoped<IQueryHandler<SearchProductsQuery, IEnumerable<ProductDto>>, SearchProductsHandler>();
        
        return services;
    }
}

// В Startup
services.AddCqrs();
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IProductReadRepository, ProductReadRepository>();
services.AddScoped<IProductValidator, ProductValidator>();
services.AddScoped<IEventPublisher, EventPublisher>();

Использование в контроллере

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ICommandDispatcher _commandDispatcher;
    private readonly IQueryDispatcher _queryDispatcher;
    
    public ProductsController(
        ICommandDispatcher commandDispatcher,
        IQueryDispatcher queryDispatcher)
    {
        _commandDispatcher = commandDispatcher;
        _queryDispatcher = queryDispatcher;
    }
    
    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        var product = await _queryDispatcher.DispatchAsync<GetProductByIdQuery, ProductDto>(
            new GetProductByIdQuery(id)
        );
        return Ok(product);
    }
    
    [HttpGet]
    public async Task<IActionResult> GetAll([FromQuery] int pageNumber = 1)
    {
        var result = await _queryDispatcher.DispatchAsync<GetAllProductsQuery, PaginatedResult<ProductDto>>(
            new GetAllProductsQuery(pageNumber, 10)
        );
        return Ok(result);
    }
    
    [HttpPost]
    public async Task<IActionResult> CreateProduct([FromBody] CreateProductRequest request)
    {
        var command = new CreateProductCommand(
            request.Name,
            request.Description,
            request.Price,
            request.StockQuantity
        );
        
        var productId = await _commandDispatcher.DispatchAsync<CreateProductCommand, int>(command);
        return CreatedAtAction(nameof(GetProduct), new { id = productId });
    }
    
    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateProduct(int id, [FromBody] UpdateProductRequest request)
    {
        var command = new UpdateProductCommand(
            id,
            request.Name,
            request.Description,
            request.Price,
            request.StockQuantity
        );
        
        await _commandDispatcher.DispatchAsync(command);
        return NoContent();
    }
    
    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteProduct(int id)
    {
        await _commandDispatcher.DispatchAsync(new DeleteProductCommand(id));
        return NoContent();
    }
    
    [HttpGet("search")]
    public async Task<IActionResult> Search(
        string searchTerm,
        decimal? minPrice,
        decimal? maxPrice)
    {
        var result = await _queryDispatcher.DispatchAsync<SearchProductsQuery, IEnumerable<ProductDto>>(
            new SearchProductsQuery(searchTerm, minPrice, maxPrice)
        );
        return Ok(result);
    }
}

Преимущества CQRS

  1. Масштабируемость — Читающие базы могут быть отдельно оптимизированы
  2. Производительность — Можно кешировать результаты запросов
  3. Разделение забот — Разработчик видит явно читает или пишет
  4. Тестируемость — Каждый handler тестируется отдельно
  5. Простота — Код становится более читаемым

Когда использовать CQRS

  • Используй для сложных доменов с большим количеством операций
  • Используй если читаний намного больше чем писаний
  • Используй для систем требующих разных моделей для чтения/записи
  • Не используй для простых CRUD приложений без особых требований
  • Не используй если команда не готова к дополнительной сложности
CQRS: Разделение команд и запросов | PrepBro