← Назад к вопросам
CQRS: Разделение команд и запросов
3.0 Senior🔥 131 комментариев
#Dependency Injection и IoC#Архитектура и микросервисы#ООП и паттерны проектирования
Условие
Реализуйте простую CQRS-архитектуру для сервиса управления продуктами.
Требования:
- Разделить операции чтения (Query) и записи (Command)
- Создать отдельные модели для чтения и записи
- Реализовать базовые интерфейсы 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
- Масштабируемость — Читающие базы могут быть отдельно оптимизированы
- Производительность — Можно кешировать результаты запросов
- Разделение забот — Разработчик видит явно читает или пишет
- Тестируемость — Каждый handler тестируется отдельно
- Простота — Код становится более читаемым
Когда использовать CQRS
- Используй для сложных доменов с большим количеством операций
- Используй если читаний намного больше чем писаний
- Используй для систем требующих разных моделей для чтения/записи
- Не используй для простых CRUD приложений без особых требований
- Не используй если команда не готова к дополнительной сложности