Введение в CQRS и Event Sourcing
В постоянно меняющемся ландшафте разработки программного обеспечения появились два паттерна, которые стали ключевыми для создания масштабируемых, удобных в обслуживании и надёжных приложений: разделение ответственности за команды и запросы (CQRS) и событийный источник данных (Event Sourcing). Эти паттерны в сочетании предлагают мощный подход к управлению сложностью современных программных систем.
Понимание CQRS
CQRS — это паттерн проектирования, который разделяет обязанности по обработке команд (записей) и запросов (чтений) на отдельные модели. Это разделение позволяет независимо масштабировать и оптимизировать операции чтения и записи, что особенно полезно в приложениях с высоким трафиком.
Модель команд (модель записи)
Модель команд отвечает за обработку операций, изменяющих состояние приложения. Она включает в себя сложную бизнес-логику и проверки, гарантируя, что изменения состояния согласованы и допустимы.
Модель запросов (модель чтения)
Модель запросов, с другой стороны, управляет операциями, связанными с извлечением данных. Она оптимизирована для производительности и простоты, часто обходя сложную бизнес-логику для обеспечения быстрого и эффективного извлечения данных.
Преимущества CQRS
- Масштабируемость: Операции чтения и записи можно масштабировать независимо в соответствии с их конкретными требованиями.
- Производительность: Модели чтения можно оптимизировать для быстрой работы запросов, а модели записи сосредоточены на поддержании консистентности данных.
- Удобство обслуживания: Разделение задач упрощает управление кодовой базой и её понимание.
Понимание событийного источника данных
Событийный источник данных — это паттерн проектирования, в котором состояние приложения хранится в виде последовательности событий. Каждое событие представляет изменение состояния, и текущее состояние получается путём воспроизведения этих событий.
Определение событий
События в событийном источнике данных являются основой состояния приложения. Вот пример того, как вы можете определить события для приложения электронной коммерции:
public class OrderCreatedEvent
{
public Guid OrderId { get; set; }
public DateTime OrderDate { get; set; }
}
public class OrderItemAddedEvent
{
public Guid OrderId { get; set; }
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
Хранение событий
Для хранения этих событий вам нужен хранилище событий. Вот простой пример хранилища событий:
public class EventStore
{
private readonly List<object> _events = new List<object>();
public void SaveEvent(object @event)
{
_events.Add(@event);
}
public IEnumerable<object> GetEvents()
{
return _events;
}
}
Обработка событий
Обработчики событий отвечают за применение этих событий к модели домена. Вот как вы можете обработать ранее определённые события:
public class OrderEventHandler
{
private readonly Dictionary<Guid, Order> _orders = new Dictionary<Guid, Order>();
public void Handle(OrderCreatedEvent @event)
{
var order = new Order
{
Id = @event.OrderId,
OrderDate = @event.OrderDate,
Items = new List<OrderItem>()
};
_orders[@event.OrderId] = order;
}
public void Handle(OrderItemAddedEvent @event)
{
if (_orders.TryGetValue(@event.OrderId, out var order))
{
var item = new OrderItem
{
ProductId = @event.ProductId,
Quantity = @event.Quantity,
UnitPrice = @event.UnitPrice
};
order.Items.Add(item);
}
}
public Order GetOrder(Guid orderId)
{
return _orders.TryGetValue(orderId, out var order) ? order : null;
}
}
Интеграция CQRS и событийного источника данных
Чтобы интегрировать CQRS и событийный источник данных, вам нужно изменить обработчики команд, чтобы они генерировали события вместо прямого изменения состояния.
Обработчики команд
Вот пример обработчика команд, который генерирует события:
public class CreateOrderCommandHandler
{
private readonly EventStore _eventStore;
public CreateOrderCommandHandler(EventStore eventStore)
{
_eventStore = eventStore;
}
public async Task Handle(CreateOrderCommand command)
{
var orderCreatedEvent = new OrderCreatedEvent
{
OrderId = Guid.NewGuid(),
OrderDate = command.OrderDate
};
_eventStore.SaveEvent(orderCreatedEvent);
foreach (var item in command.Items)
{
var orderItemAddedEvent = new OrderItemAddedEvent
{
OrderId = orderCreatedEvent.OrderId,
ProductId = item.ProductId,
Quantity = item.Quantity,
UnitPrice = item.UnitPrice
};
_eventStore.SaveEvent(orderItemAddedEvent);
}
}
}
Перестроение состояния из событий
Чтобы перестроить состояние приложения из сохранённых событий, вам нужен механизм для их воспроизведения:
public class OrderService
{
private readonly EventStore _eventStore;
private readonly OrderEventHandler _eventHandler;
public OrderService(EventStore eventStore, OrderEventHandler eventHandler)
{
_eventStore = eventStore;
_eventHandler = eventHandler;
}
public void RebuildState()
{
foreach (var @event in _eventStore.GetEvents())
{
switch (@event)
{
case OrderCreatedEvent e:
_eventHandler.Handle(e);
break;
case OrderItemAddedEvent e:
_eventHandler.Handle(e);
break;
}
}
}
public Order GetOrder(Guid orderId)
{
return _eventHandler.GetOrder(orderId);
}
}
Настройка проекта
Чтобы начать реализацию CQRS и событийного источника данных в приложении .NET Core, вам нужно настроить новый проект.
Создание проекта
dotnet new webapi -n CQRSExample
cd CQRSExample
Определение моделей домена
Для этого примера давайте рассмотрим простой домен электронной коммерции с сущностями «Заказ» и «Продукт»:
public class Order
{
public Guid Id { get; set; }
public DateTime OrderDate { get; set; }
public List<OrderItem> Items { get; set; }
public decimal TotalAmount => Items.Sum(i => i.Quantity * i.UnitPrice);
}
public class OrderItem
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
Реализация модели команд
Команды представляют действия, которые изменяют состояние системы. Вот пример команды для создания заказа:
public class CreateOrderCommand
{
public DateTime OrderDate { get; set; }
public List<OrderItemDto> Items { get; set; }
}
public class OrderItemDto
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
Использование MediatR для CQRS
MediatR — популярная библиотека для реализации паттерна CQRS в .NET Core. Вот как вы можете её использовать:
Установка MediatR
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Регистрация MediatR
В файле Startup.cs зарегистрируйте MediatR:
public void ConfigureServices(IServiceCollection services)
{
services.AddMediatR(typeof(Startup));
// Другие сервисы
}
Обработка команд с помощью MediatR
Вот как вы можете обрабатывать команды с помощью MediatR:
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly EventStore _eventStore;
public CreateOrderCommandHandler(EventStore eventStore)
{
_eventStore = eventStore;
}
public async Task<Guid> Handle(CreateOrderCommand command, CancellationToken cancellationToken)
{
var orderCreatedEvent = new OrderCreatedEvent
{
OrderId = Guid.NewGuid(),
OrderDate = command.OrderDate
};
_eventStore.SaveEvent(orderCreatedEvent);
foreach (var item in command.Items)
{
var orderItemAddedEvent = new OrderItemAddedEvent
{
OrderId = orderCreatedEvent.OrderId,
ProductId = item.ProductId,
Quantity = item.Quantity,
UnitPrice