Концепция CQRS и её роль в современной разработке
В современном мире разработки программного обеспечения архитектурные паттерны играют ключевую роль в создании масштабируемых и поддерживаемых приложений. Одним из наиболее эффективных подходов является Command Query Responsibility Segregation (CQRS) - архитектурный паттерн, который предлагает разделение операций чтения и записи данных в приложении. Этот подход становится особенно актуальным в эпоху микросервисной архитектуры и распределенных систем, где традиционные монолитные решения уже не справляются с растущими требованиями к производительности и масштабируемости.
CQRS представляет собой эволюционное развитие классической многоуровневой архитектуры, предлагая более гибкий и эффективный способ управления данными. В основе этого паттерна лежит простая, но мощная идея: операции, которые изменяют состояние системы (команды), должны быть отделены от операций, которые это состояние считывают (запросы). Такое разделение позволяет оптимизировать каждую часть системы независимо, что приводит к повышению производительности и улучшению масштабируемости приложения.
Важно понимать, что CQRS - это не просто технический паттерн, а целостный подход к проектированию архитектуры приложения. Он затрагивает все уровни системы: от пользовательского интерфейса до хранения данных, и требует тщательного планирования при внедрении. При правильной реализации этот паттерн может значительно упростить разработку сложных бизнес-приложений, особенно в случаях, когда модели чтения и записи данных существенно различаются.
В контексте современной разработки на платформе .NET, CQRS часто реализуется с использованием популярной библиотеки MediatR, которая предоставляет элегантный способ организации взаимодействия между различными компонентами системы. Этот инструмент позволяет создавать чистую и поддерживаемую архитектуру, следуя принципам CQRS без излишней сложности в реализации.
Основы CQRS
Command Query Responsibility Segregation представляет собой фундаментальный архитектурный паттерн, который кардинально меняет подход к организации взаимодействия с данными в приложении. В основе этого паттерна лежит принцип разделения ответственности между операциями чтения и записи данных. Традиционная архитектура обычно использует единую модель для обоих типов операций, что может создавать определенные сложности при масштабировании и оптимизации производительности системы.
Ключевой особенностью CQRS является четкое разграничение между командами (Commands) и запросами (Queries). Команды представляют собой операции, которые изменяют состояние системы, но не возвращают данные. Например, создание нового пользователя, обновление заказа или удаление записи - все это команды. Запросы, напротив, предназначены только для получения данных и никогда не изменяют состояние системы. Они могут возвращать данные в любом удобном формате, оптимизированном для конкретного случая использования.
В контексте реализации на платформе .NET, команды обычно представляются в виде отдельных классов, которые содержат все необходимые данные для выполнения операции. Рассмотрим пример простой команды для создания нового пользователя:
C# | 1
2
3
4
5
6
| public class CreateUserCommand : ICommand
{
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
} |
|
Запросы также реализуются в виде отдельных классов, но их структура ориентирована на получение данных:
C# | 1
2
3
4
5
6
7
8
9
10
11
| public class GetUserByIdQuery : IQuery<UserDto>
{
public int UserId { get; set; }
}
public class UserDto
{
public int Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
} |
|
В традиционной архитектуре часто используется единая модель данных как для чтения, так и для записи. Это может приводить к избыточной сложности, когда требования к представлению данных существенно отличаются от требований к их сохранению. CQRS решает эту проблему, позволяя использовать разные модели для разных целей. Модель команд может быть оптимизирована для обеспечения целостности данных и соблюдения бизнес-правил, в то время как модель запросов может быть денормализована для повышения производительности чтения.
Важным аспектом CQRS является обработка команд и запросов. Для каждой команды создается свой обработчик, который содержит всю логику выполнения операции:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand>
{
private readonly IUserRepository _repository;
public CreateUserCommandHandler(IUserRepository repository)
{
_repository = repository;
}
public async Task Handle(CreateUserCommand command)
{
var user = new User
{
Username = command.Username,
Email = command.Email,
PasswordHash = HashPassword(command.Password)
};
await _repository.CreateUser(user);
}
} |
|
Аналогично, для каждого запроса создается свой обработчик, оптимизированный для быстрого получения данных:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class GetUserByIdQueryHandler : IQueryHandler<GetUserByIdQuery, UserDto>
{
private readonly IUserReadRepository _readRepository;
public GetUserByIdQueryHandler(IUserReadRepository readRepository)
{
_readRepository = readRepository;
}
public async Task<UserDto> Handle(GetUserByIdQuery query)
{
return await _readRepository.GetUserById(query.UserId);
}
} |
|
CQRS также предоставляет возможность использовать разные хранилища данных для операций чтения и записи. Например, можно использовать реляционную базу данных для команд, обеспечивая строгую согласованность данных, и документоориентированную базу данных для запросов, оптимизированную для быстрого поиска и чтения.
При реализации CQRS важно понимать основные преимущества и потенциальные сложности этого архитектурного паттерна. Одним из ключевых преимуществ является возможность независимого масштабирования операций чтения и записи. В высоконагруженных системах часто наблюдается существенная разница между количеством операций чтения и записи, причем операции чтения обычно преобладают. CQRS позволяет оптимизировать каждую часть системы в соответствии с её специфическими требованиями.
Другим значительным преимуществом является улучшенная безопасность и контроль над данными. Поскольку операции записи четко отделены от операций чтения, становится проще реализовать детальный аудит изменений и обеспечить необходимый уровень безопасности для критически важных операций. Каждая команда может быть проверена и валидирована независимо, что упрощает поддержание целостности данных.
Однако стоит отметить, что внедрение CQRS также сопряжено с определенными вызовами. Одним из основных является необходимость обеспечения согласованности данных между моделями чтения и записи. В системах с разделенными хранилищами данных может возникнуть временная несогласованность, известная как "eventual consistency" (итоговая согласованность). Это означает, что после выполнения команды может потребоваться некоторое время, прежде чем изменения станут видимыми в модели чтения.
Рассмотрим пример реализации механизма синхронизации между моделями:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class OrderCreatedEventHandler : IEventHandler<OrderCreatedEvent>
{
private readonly IReadModelUpdateService _updateService;
public OrderCreatedEventHandler(IReadModelUpdateService updateService)
{
_updateService = updateService;
}
public async Task Handle(OrderCreatedEvent @event)
{
var readModel = new OrderReadModel
{
OrderId = @event.OrderId,
CustomerName = @event.CustomerName,
TotalAmount = @event.TotalAmount,
Status = "Created"
};
await _updateService.UpdateReadModel(readModel);
}
} |
|
При выборе CQRS как архитектурного решения важно оценить, насколько сложность его внедрения оправдана для конкретного проекта. Этот паттерн наиболее эффективен в сложных предметных областях, где модели чтения и записи существенно различаются, или в системах с высокими требованиями к производительности и масштабируемости. Для небольших приложений с простой доменной моделью внедрение CQRS может оказаться избыточным и привести к ненужному усложнению архитектуры.
Процесс внедрения CQRS требует тщательного планирования и может быть осуществлен постепенно. Начать можно с разделения интерфейсов для операций чтения и записи, сохраняя при этом единое хранилище данных. По мере роста системы и появления новых требований к производительности можно перейти к полному разделению моделей и хранилищ данных.
Компоненты CQRS
В архитектуре CQRS можно выделить несколько ключевых компонентов, каждый из которых играет важную роль в обеспечении эффективной работы системы. Понимание этих компонентов и их взаимодействия необходимо для успешной реализации паттерна. Рассмотрим подробно каждый из основных элементов архитектуры и их практическую реализацию.
Команды являются одним из фундаментальных компонентов CQRS. Они представляют собой намерение изменить состояние системы и всегда выражаются в форме императива. Команда должна содержать все необходимые данные для выполнения операции и не должна возвращать результат, за исключением информации об успешности выполнения. При реализации команд важно следовать принципу инкапсуляции и обеспечивать их неизменяемость после создания.
C# | 1
2
3
4
5
6
7
8
9
10
11
| public class UpdateProductPriceCommand : ICommand
{
public UpdateProductPriceCommand(Guid productId, decimal newPrice)
{
ProductId = productId;
NewPrice = newPrice;
}
public Guid ProductId { get; private set; }
public decimal NewPrice { get; private set; }
} |
|
Обработчики команд представляют собой классы, которые содержат бизнес-логику для выполнения конкретной команды. Каждый обработчик отвечает за валидацию входных данных, применение бизнес-правил и сохранение изменений в хранилище данных. Важной особенностью обработчиков является их изолированность - один обработчик отвечает только за одну команду.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class UpdateProductPriceCommandHandler : ICommandHandler<UpdateProductPriceCommand>
{
private readonly IProductRepository _repository;
private readonly IValidationService _validationService;
public UpdateProductPriceCommandHandler(
IProductRepository repository,
IValidationService validationService)
{
_repository = repository;
_validationService = validationService;
}
public async Task Handle(UpdateProductPriceCommand command)
{
await _validationService.ValidatePrice(command.NewPrice);
var product = await _repository.GetById(command.ProductId);
product.UpdatePrice(command.NewPrice);
await _repository.SaveChanges();
}
} |
|
Запросы являются вторым ключевым компонентом архитектуры CQRS. В отличие от команд, запросы предназначены только для получения данных и никогда не изменяют состояние системы. Они могут содержать параметры для фильтрации, сортировки и пагинации данных. Важной особенностью запросов является их ориентация на конкретные потребности представления данных.
C# | 1
2
3
4
5
6
7
8
9
10
11
| public class GetProductDetailsQuery : IQuery<ProductDetailsDto>
{
public GetProductDetailsQuery(Guid productId, string cultureName)
{
ProductId = productId;
CultureName = cultureName;
}
public Guid ProductId { get; private set; }
public string CultureName { get; private set; }
} |
|
Обработчики запросов отвечают за получение и преобразование данных в формат, необходимый клиенту. Они могут использовать оптимизированные запросы к базе данных, кэширование и другие механизмы для повышения производительности. Важно отметить, что обработчики запросов работают с отдельной моделью чтения, которая может быть денормализована для улучшения производительности.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class GetProductDetailsQueryHandler : IQueryHandler<GetProductDetailsQuery, ProductDetailsDto>
{
private readonly IProductReadRepository _readRepository;
private readonly ICultureService _cultureService;
public GetProductDetailsQueryHandler(
IProductReadRepository readRepository,
ICultureService cultureService)
{
_readRepository = readRepository;
_cultureService = cultureService;
}
public async Task<ProductDetailsDto> Handle(GetProductDetailsQuery query)
{
var culture = await _cultureService.GetCulture(query.CultureName);
return await _readRepository.GetProductDetails(query.ProductId, culture);
}
} |
|
Модели данных в архитектуре CQRS разделяются на модели записи и модели чтения. Модели записи отражают бизнес-правила и обеспечивают целостность данных, в то время как модели чтения оптимизированы для конкретных сценариев использования. Такое разделение позволяет независимо развивать и оптимизировать каждую из моделей.
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Модель записи
public class Product
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public decimal Price { get; private set; }
public List<PriceHistory> PriceHistory { get; private set; }
public void UpdatePrice(decimal newPrice)
{
PriceHistory.Add(new PriceHistory(Price, DateTime.UtcNow));
Price = newPrice;
}
}
// Модель чтения
public class ProductDetailsDto
{
public Guid Id { get; set; }
public string LocalizedName { get; set; }
public decimal CurrentPrice { get; set; }
public decimal PreviousPrice { get; set; }
public string CurrencySymbol { get; set; }
} |
|
Важным компонентом архитектуры CQRS является система событий, которая обеспечивает согласованность между моделями чтения и записи. События представляют собой записи о произошедших изменениях в системе и используются для обновления моделей чтения. Рассмотрим пример реализации события и его обработчика:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class ProductPriceUpdatedEvent : IEvent
{
public Guid ProductId { get; private set; }
public decimal OldPrice { get; private set; }
public decimal NewPrice { get; private set; }
public DateTime UpdatedAt { get; private set; }
public ProductPriceUpdatedEvent(Guid productId, decimal oldPrice, decimal newPrice)
{
ProductId = productId;
OldPrice = oldPrice;
NewPrice = newPrice;
UpdatedAt = DateTime.UtcNow;
}
} |
|
Для обработки событий создаются специальные обработчики, которые отвечают за обновление моделей чтения:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class ProductPriceUpdatedEventHandler : IEventHandler<ProductPriceUpdatedEvent>
{
private readonly IReadModelUpdater _readModelUpdater;
private readonly ICacheInvalidator _cacheInvalidator;
public ProductPriceUpdatedEventHandler(
IReadModelUpdater readModelUpdater,
ICacheInvalidator cacheInvalidator)
{
_readModelUpdater = readModelUpdater;
_cacheInvalidator = cacheInvalidator;
}
public async Task Handle(ProductPriceUpdatedEvent @event)
{
await _readModelUpdater.UpdateProductPrice(@event.ProductId, @event.NewPrice);
await _cacheInvalidator.InvalidateProductCache(@event.ProductId);
}
} |
|
Особое внимание в архитектуре CQRS уделяется валидации данных. Валидация выполняется на уровне команд перед их выполнением, что позволяет гарантировать целостность данных. Для этого используются специальные валидаторы:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class UpdateProductPriceCommandValidator : IValidator<UpdateProductPriceCommand>
{
public Task<ValidationResult> Validate(UpdateProductPriceCommand command)
{
var result = new ValidationResult();
if (command.NewPrice <= 0)
{
result.AddError("Price must be greater than zero");
}
if (command.ProductId == Guid.Empty)
{
result.AddError("Invalid product identifier");
}
return Task.FromResult(result);
}
} |
|
Для обеспечения эффективной работы с данными в CQRS часто используются специализированные репозитории для операций чтения и записи. Это позволяет оптимизировать запросы и обеспечить необходимую производительность системы:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
| public interface IProductReadRepository
{
Task<ProductDetailsDto> GetProductDetails(Guid productId, CultureInfo culture);
Task<IEnumerable<ProductListItemDto>> GetProductList(ProductFilterCriteria criteria);
}
public interface IProductWriteRepository
{
Task<Product> GetById(Guid productId);
Task Save(Product product);
Task Delete(Guid productId);
} |
|
Каждый из этих компонентов играет важную роль в обеспечении надежной и эффективной работы системы, построенной на принципах CQRS. Их правильная организация и взаимодействие позволяют создавать масштабируемые и производительные приложения, способные обрабатывать сложные бизнес-процессы.
MediatR как инструмент реализации
MediatR представляет собой мощную библиотеку для реализации паттерна медиатор в .NET-приложениях, которая идеально подходит для имплементации архитектуры CQRS. Эта библиотека существенно упрощает процесс организации взаимодействия между различными компонентами системы, обеспечивая слабую связанность и улучшая поддерживаемость кода.
Для начала работы с MediatR необходимо установить соответствующий NuGet-пакет и настроить зависимости в проекте. Это делается путем добавления необходимых сервисов в контейнер зависимостей:
C# | 1
2
3
4
5
6
| public void ConfigureServices(IServiceCollection services)
{
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
} |
|
После установки и настройки MediatR можно приступать к определению команд и запросов. Библиотека предоставляет интерфейсы `IRequest<T>` для запросов и `IRequest` для команд. Рассмотрим пример создания команды с использованием MediatR:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| public class CreateProductCommand : IRequest<Guid>
{
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public string Category { get; set; }
}
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Guid>
{
private readonly IProductRepository _repository;
public CreateProductCommandHandler(IProductRepository repository)
{
_repository = repository;
}
public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
var product = new Product
{
Name = request.Name,
Price = request.Price,
Description = request.Description,
Category = request.Category
};
await _repository.CreateProduct(product);
return product.Id;
}
} |
|
Одним из ключевых преимуществ MediatR является возможность реализации пайплайнов обработки запросов. Это позволяет добавлять сквозную функциональность, такую как валидация, логирование или кэширование, без изменения основной логики обработчиков. Рассмотрим пример реализации поведения для валидации:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken)));
var failures = validationResults
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count != 0)
{
throw new ValidationException(failures);
}
}
return await next();
}
} |
|
MediatR также поддерживает работу с уведомлениями, что особенно полезно при реализации событийно-ориентированной архитектуры. Уведомления могут иметь несколько обработчиков, что позволяет реализовать различные реакции на одно событие:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public class ProductCreatedNotification : INotification
{
public Guid ProductId { get; }
public string ProductName { get; }
public ProductCreatedNotification(Guid productId, string productName)
{
ProductId = productId;
ProductName = productName;
}
}
public class EmailNotificationHandler : INotificationHandler<ProductCreatedNotification>
{
private readonly IEmailService _emailService;
public EmailNotificationHandler(IEmailService emailService)
{
_emailService = emailService;
}
public async Task Handle(ProductCreatedNotification notification, CancellationToken cancellationToken)
{
await _emailService.SendProductCreatedNotification(notification.ProductId, notification.ProductName);
}
} |
|
При работе с MediatR важно правильно организовать взаимодействие между контроллерами и обработчиками. В контроллерах достаточно внедрить экземпляр `IMediator` и использовать его для отправки команд и запросов:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| [ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<ActionResult<Guid>> CreateProduct([FromBody] CreateProductCommand command)
{
var productId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetProduct), new { id = productId }, productId);
}
[HttpGet("{id}")]
public async Task<ActionResult<ProductDto>> GetProduct(Guid id)
{
var query = new GetProductByIdQuery(id);
var result = await _mediator.Send(query);
return Ok(result);
}
} |
|
Практическая реализация
При реализации CQRS в реальном проекте необходимо тщательно продумать структуру проекта и организацию кодовой базы. Рассмотрим пример практической реализации CQRS с использованием MediatR в проекте на C#. Начнем с создания базовой структуры проекта, которая будет включать все необходимые компоненты для эффективной работы системы.
Первым шагом является организация проекта в соответствии с принципами чистой архитектуры. Создадим следующую структуру решения:
Код:
Solution
├── Domain
│ ├── Entities
│ ├── ValueObjects
│ └── Events
├── Application
│ ├── Commands
│ ├── Queries
│ ├── DTOs
│ └── Interfaces
├── Infrastructure
│ ├── Persistence
│ ├── Services
│ └── External
└── WebApi
├── Controllers
└── Middleware
После создания структуры проекта необходимо реализовать базовые интерфейсы и абстракции. Начнем с определения основных сущностей домена:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| public abstract class Entity
{
public Guid Id { get; protected set; }
private List<IDomainEvent> _domainEvents = new();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
public class Order : Entity
{
public string CustomerName { get; private set; }
public decimal TotalAmount { get; private set; }
public OrderStatus Status { get; private set; }
public List<OrderItem> Items { get; private set; }
public void AddItem(Product product, int quantity)
{
var item = new OrderItem(product, quantity);
Items.Add(item);
TotalAmount += item.SubTotal;
AddDomainEvent(new OrderItemAddedEvent(this.Id, product.Id, quantity));
}
} |
|
Далее реализуем базовые интерфейсы для работы с хранилищем данных:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public interface IUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
}
public interface IRepository<T> where T : Entity
{
Task<T> GetByIdAsync(Guid id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
} |
|
Теперь реализуем конкретные команды и запросы для работы с заказами:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| public class CreateOrderCommand : IRequest<Guid>
{
public string CustomerName { get; set; }
public List<OrderItemDto> Items { get; set; }
}
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
private readonly IUnitOfWork _unitOfWork;
public CreateOrderCommandHandler(
IOrderRepository orderRepository,
IProductRepository productRepository,
IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
_unitOfWork = unitOfWork;
}
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order(request.CustomerName);
foreach (var item in request.Items)
{
var product = await _productRepository.GetByIdAsync(item.ProductId);
order.AddItem(product, item.Quantity);
}
await _orderRepository.AddAsync(order);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return order.Id;
}
} |
|
Для обработки запросов реализуем соответствующие классы:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class GetOrderDetailsQuery : IRequest<OrderDetailsDto>
{
public Guid OrderId { get; set; }
}
public class GetOrderDetailsQueryHandler : IRequestHandler<GetOrderDetailsQuery, OrderDetailsDto>
{
private readonly IOrderReadRepository _orderReadRepository;
public GetOrderDetailsQueryHandler(IOrderReadRepository orderReadRepository)
{
_orderReadRepository = orderReadRepository;
}
public async Task<OrderDetailsDto> Handle(GetOrderDetailsQuery request, CancellationToken cancellationToken)
{
return await _orderReadRepository.GetOrderDetailsAsync(request.OrderId);
}
} |
|
Важным аспектом практической реализации является обработка ошибок и валидация. Создадим собственные исключения и валидаторы:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderCommandValidator()
{
RuleFor(x => x.CustomerName)
.NotEmpty().WithMessage("Customer name is required")
.MaximumLength(100).WithMessage("Customer name must not exceed 100 characters");
RuleFor(x => x.Items)
.NotEmpty().WithMessage("Order must contain at least one item");
RuleForEach(x => x.Items).SetValidator(new OrderItemDtoValidator());
}
}
public class OrderNotFoundException : Exception
{
public OrderNotFoundException(Guid orderId)
: base($"Order with ID {orderId} was not found.")
{
}
} |
|
Для эффективной работы с базой данных реализуем специализированные репозитории:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public class OrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _context;
public OrderRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Order> GetByIdAsync(Guid id)
{
var order = await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == id);
if (order == null)
throw new OrderNotFoundException(id);
return order;
}
public async Task AddAsync(Order order)
{
await _context.Orders.AddAsync(order);
}
} |
|
Для обработки событий домена реализуем соответствующие обработчики:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| public class OrderCreatedDomainEventHandler : INotificationHandler<OrderCreatedDomainEvent>
{
private readonly IEmailService _emailService;
private readonly ILogger<OrderCreatedDomainEventHandler> _logger;
public OrderCreatedDomainEventHandler(
IEmailService emailService,
ILogger<OrderCreatedDomainEventHandler> logger)
{
_emailService = emailService;
_logger = logger;
}
public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
{
try
{
await _emailService.SendOrderConfirmationEmail(
notification.OrderId,
notification.CustomerEmail);
_logger.LogInformation(
"Order confirmation email sent for order {OrderId}",
notification.OrderId);
}
catch (Exception ex)
{
_logger.LogError(
ex,
"Failed to send order confirmation email for order {OrderId}",
notification.OrderId);
}
}
} |
|
Примеры использования
Рассмотрим практические примеры внедрения CQRS в различных типах приложений. В первую очередь, обратим внимание на реализацию в системе электронной коммерции, где этот архитектурный паттерн может принести значительную пользу. В таких системах существует явное разделение между операциями чтения (просмотр каталога, поиск товаров) и записи (оформление заказов, обновление inventory).
В контексте e-commerce платформы, CQRS может быть реализован следующим образом:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| public class CreateOrderCommand : IRequest<Guid>
{
public List<OrderItemDto> Items { get; set; }
public DeliveryDetails DeliveryDetails { get; set; }
public PaymentInfo PaymentInfo { get; set; }
}
public class GetProductCatalogQuery : IRequest<PaginatedResult<ProductDto>>
{
public ProductFilter Filter { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
} |
|
Второй пример демонстрирует применение CQRS в банковской системе, где особенно важна точность и надежность операций с данными. Здесь команды используются для выполнения транзакций, а запросы - для получения информации о состоянии счетов и истории операций:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| public class TransferMoneyCommand : IRequest<TransactionResult>
{
public Guid FromAccountId { get; set; }
public Guid ToAccountId { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; }
}
public class GetAccountBalanceQuery : IRequest<AccountBalanceDto>
{
public Guid AccountId { get; set; }
public bool IncludeTransactionHistory { get; set; }
} |
|
В системах управления контентом CQRS позволяет эффективно разделить операции создания и редактирования контента от операций его чтения и отображения. Рассмотрим пример реализации:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public class PublishArticleCommand : IRequest<PublishResult>
{
public Guid ArticleId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public List<string> Tags { get; set; }
public PublishingSettings Settings { get; set; }
}
public class GetArticlesByTagQuery : IRequest<List<ArticlePreviewDto>>
{
public string Tag { get; set; }
public ArticleSortingOptions SortBy { get; set; }
public bool IncludeUnpublished { get; set; }
} |
|
В логистических приложениях CQRS помогает эффективно управлять сложными процессами доставки и отслеживания грузов. Здесь особенно важна способность системы обрабатывать большое количество параллельных запросов на получение статуса доставки:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| public class UpdateShipmentStatusCommand : IRequest<Unit>
{
public Guid ShipmentId { get; set; }
public ShipmentStatus NewStatus { get; set; }
public GeoLocation CurrentLocation { get; set; }
public string StatusDescription { get; set; }
}
public class GetShipmentTrackingQuery : IRequest<ShipmentTrackingDto>
{
public string TrackingNumber { get; set; }
public bool IncludeFullHistory { get; set; }
} |
|
Наконец, рассмотрим пример использования CQRS в системе бронирования, где важна точность данных и способность системы справляться с пиковыми нагрузками:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class CreateReservationCommand : IRequest<ReservationResult>
{
public Guid ResourceId { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public ReservationDetails Details { get; set; }
}
public class CheckResourceAvailabilityQuery : IRequest<AvailabilityInfo>
{
public Guid ResourceId { get; set; }
public DateTimeRange TimeRange { get; set; }
public List<ResourceRequirement> Requirements { get; set; }
} |
|
В каждом из этих примеров CQRS помогает решить специфические проблемы предметной области. Важно отметить, что реализация включает не только определение команд и запросов, но и соответствующую инфраструктуру для их обработки и валидации:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| public class ReservationValidationBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IAvailabilityChecker _availabilityChecker;
public ReservationValidationBehavior(IAvailabilityChecker availabilityChecker)
{
_availabilityChecker = availabilityChecker;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (request is CreateReservationCommand reservationCommand)
{
var isAvailable = await _availabilityChecker.CheckAvailability(
reservationCommand.ResourceId,
reservationCommand.StartTime,
reservationCommand.EndTime);
if (!isAvailable)
throw new ResourceNotAvailableException(reservationCommand.ResourceId);
}
return await next();
}
} |
|
Каждый из представленных примеров также должен учитывать вопросы производительности и масштабируемости. Например, в банковской системе критически важно правильно организовать кэширование данных для быстрого доступа к информации о счетах:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class AccountBalanceQueryHandler : IRequestHandler<GetAccountBalanceQuery, AccountBalanceDto>
{
private readonly IAccountReadRepository _repository;
private readonly ICacheService _cacheService;
public async Task<AccountBalanceDto> Handle(GetAccountBalanceQuery request, CancellationToken cancellationToken)
{
var cacheKey = $"account_balance_{request.AccountId}";
var cachedBalance = await _cacheService.GetAsync<AccountBalanceDto>(cacheKey);
if (cachedBalance != null)
return cachedBalance;
var balance = await _repository.GetBalanceAsync(request.AccountId);
await _cacheService.SetAsync(cacheKey, balance, TimeSpan.FromMinutes(5));
return balance;
}
} |
|
В системе управления контентом важно обеспечить эффективное управление версиями документов и их публикацией. Для этого можно использовать специализированные обработчики событий:
C# | 1
2
3
4
5
6
7
8
9
10
11
| public class ArticlePublishedEventHandler : INotificationHandler<ArticlePublishedEvent>
{
private readonly ISearchIndexService _searchIndex;
private readonly ICacheInvalidator _cacheInvalidator;
public async Task Handle(ArticlePublishedEvent notification, CancellationToken cancellationToken)
{
await _searchIndex.IndexArticle(notification.ArticleId);
await _cacheInvalidator.InvalidateArticleCache(notification.ArticleId);
}
} |
|
В логистических системах особое внимание следует уделять обработке геолокационных данных и оптимизации маршрутов. CQRS позволяет эффективно разделить эти операции:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class OptimizeRouteCommandHandler : IRequestHandler<OptimizeRouteCommand, OptimizedRouteResult>
{
private readonly IRouteOptimizationService _optimizationService;
private readonly IShipmentRepository _shipmentRepository;
public async Task<OptimizedRouteResult> Handle(OptimizeRouteCommand request, CancellationToken cancellationToken)
{
var shipments = await _shipmentRepository.GetActiveShipments();
var optimizedRoute = await _optimizationService.OptimizeRoute(
shipments,
request.VehicleCapacity,
request.TimeConstraints);
return new OptimizedRouteResult
{
Route = optimizedRoute,
EstimatedTime = optimizedRoute.CalculateEstimatedTime(),
FuelEfficiency = optimizedRoute.CalculateFuelEfficiency()
};
}
} |
|
В каждом из этих примеров важно обеспечить надежную обработку ошибок и поддержку транзакционности там, где это необходимо. Для этого можно использовать специальные декораторы или поведения в пайплайне обработки команд:
C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| public class TransactionalBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IUnitOfWork _unitOfWork;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
try
{
await _unitOfWork.BeginTransactionAsync();
var response = await next();
await _unitOfWork.CommitTransactionAsync();
return response;
}
catch
{
await _unitOfWork.RollbackTransactionAsync();
throw;
}
}
} |
|
Рекомендации по внедрению CQRS
При внедрении архитектуры CQRS в существующий проект или при создании нового решения важно следовать определенным рекомендациям, которые помогут избежать типичных ошибок и максимизировать преимущества данного подхода. В первую очередь, необходимо тщательно оценить необходимость внедрения CQRS, поскольку этот паттерн может оказаться избыточным для простых приложений с несложной бизнес-логикой.
Рекомендуется начинать внедрение CQRS постепенно, выделяя отдельные модули или компоненты системы, где разделение операций чтения и записи принесет наибольшую пользу. Такой подход позволяет минимизировать риски и оценить эффективность паттерна на практике. Особое внимание следует уделить обучению команды разработчиков принципам работы с CQRS и связанными технологиями, такими как MediatR.
При проектировании системы важно правильно определить границы между различными компонентами и установить четкие правила взаимодействия между ними. Следует избегать излишней сложности в командах и запросах, придерживаясь принципа единственной ответственности. Каждая команда или запрос должны выполнять только одну конкретную операцию, что упрощает поддержку и тестирование системы.
Особое внимание следует уделить обработке исключительных ситуаций и валидации данных. Рекомендуется реализовать централизованную систему валидации с использованием специализированных валидаторов для каждой команды. Также важно обеспечить надежный механизм обработки событий домена, который позволит поддерживать согласованность данных между различными компонентами системы.
Для обеспечения производительности рекомендуется использовать механизмы кэширования для часто запрашиваемых данных и оптимизировать запросы к базе данных. При работе с большими объемами данных следует рассмотреть возможность использования специализированных хранилищ для операций чтения, оптимизированных под конкретные сценарии использования. |