воскресенье, 31 мая 2026 г.

MediatR

MediatR: общее описание

MediatR — это лёгкая библиотека для .NET, реализующая паттерн «Посредник» (Mediator Pattern) и облегчающая реализацию паттерна «Команда» (Command Pattern) и «Запрос» (Query Pattern). Её основная задача — уменьшить связность между компонентами приложения, заменяя прямые вызовы на отправку сообщений через посредника.

Основные концепции

  1. Посредник (Mediator) — центральный объект, который принимает сообщения и направляет их соответствующим обработчикам.

  2. Сообщения (Messages) — команды, запросы или уведомления, которые передаются через посредника.

  3. Обработчики (Handlers) — классы, которые обрабатывают конкретные типы сообщений.

Ключевые паттерны, поддерживаемые MediatR

  • Command Pattern — для операций, изменяющих состояние системы (например, «Создать пользователя»).

  • Query Pattern — для операций чтения данных (например, «Получить список пользователей»).

  • Notification Pattern — для событий, на которые могут подписаться несколько обработчиков (например, «Пользователь зарегистрирован»).


MediatR в .NET Core: детальное описание

Установка и настройка

  1. Установите пакет через NuGet:

bash
dotnet add package MediatR

Для интеграции с ASP.NET Core дополнительно установите:

bash
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
  1. Зарегистрируйте сервисы в Program.cs (для .NET 6+):

csharp
var builder = WebApplication.CreateBuilder(args);

// Регистрация сервисов MediatR
builder.Services.AddMediatR(typeof(Program));

var app = builder.Build();

Основные типы сообщений

  1. Команды (Commands) — операции, изменяющие состояние. Реализуются через IRequest<TResponse>:

csharp
public record CreateUserCommand(string Name, string Email) : IRequest<Guid>;
  1. Запросы (Queries) — операции чтения. Также используют IRequest<TResponse>:

csharp
public record GetUserQuery(Guid Id) : IRequest<UserDto>;
  1. Уведомления (Notifications) — события, на которые может быть несколько подписчиков. Используют INotification:

csharp
public record UserCreatedNotification(Guid UserId, string Email) : INotification;

Создание обработчиков

  1. Для команд и запросов — реализуйте IRequestHandler<TRequest, TResponse>:

csharp
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Guid>
{
    public Task<Guid> Handle(CreateUserCommand request, CancellationToken cancellationToken)
    {
        // Логика создания пользователя
        var userId = Guid.NewGuid();
        // Сохранение в БД и т. д.
        return Task.FromResult(userId);
    }
}
  1. Для уведомлений — реализуйте INotificationHandler<TNotification>:

csharp
public class UserCreatedNotificationHandler : INotificationHandler<UserCreatedNotification>
{
    public Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
    {
        // Отправка email, логирование и т. д.
        Console.WriteLine($"User {notification.Email} created");
        return Task.CompletedTask;
    }
}

Использование в контроллерах ASP.NET Core

Внедрите IMediator в контроллер и используйте для отправки сообщений:

csharp
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IMediator _mediator;

    public UsersController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost]
    public async Task<ActionResult<Guid>> CreateUser(CreateUserCommand command)
    {
        var userId = await _mediator.Send(command);
        return Ok(userId);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetUser(Guid id)
    {
        var query = new GetUserQuery(id);
        var user = await _mediator.Send(query);
        return Ok(user);
    }
}

Расширенные возможности MediatR

Посредники (Behaviors)

Behaviors — это механизмы для добавления кросс‑функциональной логики (логирование, валидация, транзакции) до/после обработки сообщений.

Пример поведения для логирования:

csharp
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Starting {typeof(TRequest).Name}");
        var response = await next();
        Console.WriteLine($"Completed {typeof(TRequest).Name}");
        return response;
    }
}

Регистрация в Program.cs:

csharp
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));

Валидация с FluentValidation

Можно интегрировать FluentValidation для автоматической валидации команд и запросов:

csharp
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
    public CreateUserCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Email).EmailAddress();
    }
}

Поддержка CQRS

MediatR отлично подходит для реализации CQRS:

  • команды (IRequest<T>) — для записи;

  • запросы (IRequest<T>) — для чтения;

  • уведомления (INotification) — для событий.


Преимущества использования MediatR в .NET Core

  1. Снижение связности — компоненты не зависят друг от друга напрямую.

  2. Упрощение тестирования — обработчики легко тестируются изолированно.

  3. Разделение ответственности — чёткое разделение команд и запросов.

  4. Кросс‑функциональная логика — behaviors позволяют добавлять логирование, валидацию и т. д. без изменения бизнес‑логики.

  5. Гибкость — легко добавлять новые команды, запросы и обработчики.

  6. Поддержка событий — уведомления позволяют реализовать event‑driven архитектуру.

  7. Интеграция с DI — полная поддержка встроенного DI‑контейнера .NET Core.

  8. Асинхронность — встроенная поддержка async/await.


Недостатки и ограничения

  1. Дополнительный слой абстракции — может усложнить понимание кода для новичков.

  2. Отладка — сложнее отследить поток выполнения из‑за посредника.

  3. Производительность — небольшой оверхед из‑за дополнительного слоя диспетчеризации.

  4. Переусложнение — для простых CRUD‑приложений может быть избыточным.

  5. Ошибки конфигурации — неправильная регистрация обработчиков приводит к ошибкам во время выполнения.


Практические рекомендации

  1. Структурируйте проект по вертикальным срезам (Vertical Slice Architecture):

Features/
  Users/
    Commands/
      CreateUser/
        CreateUserCommand.cs
        CreateUserCommandHandler.cs
    Queries/
      GetUser/
        GetUserQuery.cs
        GetUserQueryHandler.cs
  1. Используйте behaviors для кросс‑функциональных задач:

    • логирование;

    • валидация;

    • транзакции;

    • кэширование.

  2. Следуйте соглашениям:

    • команды — *Command;

    • запросы — *Query;

    • обработчики — *Handler.

  3. Тестируйте обработчики изолированно — каждый обработчик должен тестироваться отдельно.

  4. Ограничьте доступ к БД — в обработчиках используйте репозитории или сервисы, а не прямое обращение к БД.

  5. Используйте CQRS‑подход — разделяйте команды и запросы, даже если используете один класс IRequest<T>.

  6. Контролируйте размер сообщений — не передавайте избыточные данные в командах и запросах.


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

Подходит для:

  • средних и крупных приложений с сложной бизнес‑логикой;

  • CQRS‑архитектур;

  • event‑driven систем;

  • микросервисных архитектур;

  • приложений с требованиями к тестируемости и поддерживаемости.

Не подходит для:

  • простых CRUD‑приложений;

  • высокопроизводительных систем с жёсткими требованиями к задержкам;

  • проектов с ограниченными ресурсами и сроками;

  • сценариев, где прямая связь компонентов предпочтительнее.

FluentValidation

FluentValidation

CQRS, Event Sourcing

CQRS, Event Sourcing

Совместное использование CQRS и Event Sourcing: подробное описание

CQRS (Command Query Responsibility Segregation) и Event Sourcing (источники событий) — два архитектурных паттерна, которые часто используют вместе для построения масштабируемых и гибких систем. Разберём их совместное применение детально.

Как они дополняют друг друга

  • CQRS разделяет операции чтения (Queries) и записи (Commands), позволяя оптимизировать каждую сторону под свои задачи.

  • Event Sourcing хранит все изменения состояния системы как последовательность неизменяемых событий.

Вместе они создают архитектуру, где:

  • команды обрабатываются через Event Sourcing — генерируют события и сохраняют их в Event Store;

  • запросы обслуживаются через оптимизированные проекции (Read Models), построенные на основе этих событий.

Компоненты совместной архитектуры

  1. Command Handler — принимает команды, валидирует их и передаёт агрегату.

  2. Aggregate — бизнес‑объект, который:

    • применяет бизнес‑правила;

    • генерирует события (OrderCreated, PaymentProcessed и т. д.);

    • восстанавливает своё состояние путём воспроизведения событий из Event Store.

  3. Event Store — специализированное хранилище для событий. Гарантирует:

    • неизменяемость событий;

    • упорядоченность по времени;

    • атомарную запись группы событий.

  4. Event Processor / Projector — читает новые события из Event Store и обновляет проекции.

  5. Read Models (проекции) — материализованные представления данных, оптимизированные под конкретные запросы. Могут быть:

    • реляционными таблицами;

    • JSON‑документами;

    • кэшами в памяти;

    • индексами поиска.

  6. Query Handler — обрабатывает запросы, обращаясь к готовым проекциям.


Пошаговый процесс работы системы

Сценарий: оформление заказа в интернет‑магазине

  1. Команда (Command): клиент отправляет команду CreateOrder с данными заказа.

  2. Обработка команды:

    • Command Handler получает команду;

    • передаёт её агрегату Order;

    • агрегат проверяет бизнес‑правила (достаточно ли товара на складе, корректны ли данные).

  3. Генерация событий: если команда валидна, агрегат генерирует события:

    • OrderCreated;

    • InventoryReserved (если товар есть в наличии).

  4. Сохранение событий: события записываются в Event Store в рамках одной транзакции.

  5. Обновление состояния агрегата: агрегат применяет события к своему внутреннему состоянию.

  6. Уведомление о событиях: система оповещает подписчиков о новых событиях.

  7. Обновление проекций:

    • Event Processor считывает новые события;

    • обновляет соответствующие Read Models:

      • таблицу «Активные заказы»;

      • JSON‑документ для карточки заказа;

      • индекс поиска по заказам.

  8. Запрос (Query): клиент запрашивает список своих заказов.

  9. Обработка запроса:

    • Query Handler обращается к оптимизированной проекции;

    • возвращает данные клиенту за миллисекунды.


Преимущества совместного использования

  1. Масштабируемость:

    • чтение и запись масштабируются независимо;

    • можно добавить больше серверов для обработки запросов или команд.

  2. Производительность:

    • запросы обслуживаются из оптимизированных проекций (быстрые чтения);

    • команды обрабатываются асинхронно, не блокируя запросы.

  3. Аудит и трассировка:

    • полная история изменений доступна через Event Store;

    • можно отследить, кто, когда и почему изменил данные.

  4. Гибкость:

    • новые проекции можно создавать без изменения бизнес‑логики;

    • разные представления данных для разных клиентов (веб, мобильное приложение, API для партнёров);

    • легко добавлять новые сервисы, подписывающиеся на события.

  5. Устойчивость к изменениям:

    • изменения схемы данных не требуют миграции старых событий;

    • новые поля можно добавлять в новые события.

  6. Интеграция:

    • события служат единой шиной данных для коммуникации между сервисами;

    • другие системы могут подписываться на события без изменения основной логики.

Недостатки и сложности

  1. Сложность архитектуры:

    • больше компонентов, чем в традиционном CRUD;

    • требуется глубокое понимание паттернов.

  2. Конечная согласованность (Eventual Consistency):

    • между записью события и обновлением проекции может быть задержка;

    • клиент может не увидеть последние изменения сразу.

  3. Производительность восстановления агрегатов:

    • для агрегатов с длинной историей событий восстановление состояния может быть медленным;

    • требуется механизм снимков (Snapshots) для ускорения.

  4. Управление версиями событий:

    • изменение структуры событий требует стратегий миграции проекций;

    • старые события могут иметь другую схему.

  5. Объём данных:

    • хранение всех событий приводит к быстрому росту объёма данных;

    • нужны стратегии архивирования и очистки.

  6. Отладка и тестирование:

    • сложнее отлаживать систему из‑за асинхронности;

    • тестирование требует симуляции потоков событий.


Практические рекомендации по реализации

  1. Используйте снимки (Snapshots):

    • сохраняйте периодические снимки состояния агрегатов;

    • при восстановлении загружайте последний снимок и применяйте только новые события после него.

  2. Идемпотентные обработчики:

    • проектируйте обработчики событий так, чтобы повторная обработка одного и того же события не приводила к ошибкам;

    • используйте уникальные идентификаторы событий для отслеживания.

  3. Компенсация ошибок:

    • реализуйте механизм компенсации (Compensating Events) для отмены некорректных действий;

    • например, событие OrderCancelled может компенсировать OrderCreated.

  4. Мониторинг и аудит:

    • логируйте все события и команды для отладки;

    • отслеживайте задержки между записью и чтением (Lag Monitoring).

  5. Стратегии обновления проекций:

    • синхронное обновление — для критически важных данных с низкой задержкой;

    • асинхронное обновление — для сложных проекций и высокой нагрузки.

  6. Разделение ответственности:

    • чётко разграничьте зоны ответственности между агрегатами;

    • избегайте слишком больших агрегатов с множеством событий.

  7. Тестирование:

    • тестируйте восстановление агрегатов из событий;

    • симулируйте задержки и сбои в обновлении проекций.


Когда использовать связку CQRS + Event Sourcing

Подходит для:

  • высоконагруженных систем с асимметричной нагрузкой на чтение/запись;

  • финансовых систем, где критичен аудит и трассировка изменений;

  • сложных бизнес‑процессов с множеством правил и согласований;

  • микросервисных архитектур с необходимостью интеграции между сервисами;

  • систем аналитики и мониторинга в реальном времени;

  • приложений с требованиями к гибкости и масштабируемости.

Не подходит для:

  • простых CRUD‑приложений (блоги, каталоги);

  • систем с жёсткими требованиями к мгновенной согласованности данных;

  • проектов с ограниченными ресурсами и сроками;

  • сценариев с частыми изменениями схемы данных без истории.


Пример: банковский счёт

Традиционный подход:

  • таблица Accounts с колонками id, balance;

  • операция «Пополнить на 100 руб.»UPDATE Accounts SET balance = balance + 100.

CQRS + Event Sourcing:

Сторона записи (Write Side):

  • команда DepositMoney (ID счёта, сумма);

  • агрегат Account генерирует событие MoneyDeposited (сумма, дата);

  • событие сохраняется в Event Store.

Сторона чтения (Read Side):

  • проекция AccountBalanceView обновляется при получении события MoneyDeposited;

  • запрос GetAccountBalance возвращает данные из AccountBalanceView.

Event Store:

  1. AccountOpened (сумма: 0 руб.)

  2. MoneyDeposited (сумма: 500 руб.)

  3. MoneyWithdrawn (сумма: 200 руб.)

  4. MoneyDeposited (сумма: 800 руб.)

Восстановление состояния:

  • начальное состояние: 0 руб.;

  • 0 + 500 = 500 руб.;

  • 500 − 200 = 300 руб.;

  • 300 + 800 = 1 100 руб.

Read Model (AccountBalanceView):

json
{
  "accountId": "123",
  "balance": 1100,
  "lastUpdated": "2023-10-05T10:00:00Z"
}