Введение в 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