Публикация событий домена с помощью MediatR

При реализации бизнес-логики внутри обработчика вы можете захотеть выполнить некоторые действия в качестве побочного эффекта обработки.

Наличие этих действий в обработчике может привести к раздуванию кода или дублированию, если вы захотите выполнить некоторые из них в нескольких обработчиках.

MediatR Notifications может пригодиться в таких случаях, давайте посмотрим, как это сделать!

Настройка

Сначала давайте создадим наш пример. Мы создадим простой веб-API с использованием .NET 6:

~$ dotnet new webapi -o MediatrNotification
Вход в полноэкранный режим Выйдите из полноэкранного режима

В своем любимом редакторе удалите все вхождения начального шаблона (WeatherForecast.cs и Controllers/WheatherForecastController.cs).

Наконец, добавьте MediatR и инициализируйте его:

~$ dotnet add package MediatR
~$ dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Войти в полноэкранный режим Выход из полноэкранного режима
// Program.cs
+ using MediatR;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
+ builder.Services.AddMediatR(typeof(Program));
Войти в полноэкранный режим Выход из полноэкранного режима

Пример использования

Для нашей демонстрации у нас будет одна конечная точка, позволяющая кому-то заказать букет цветов.

Основываясь на этом примере, мы можем создать наш запрос в новом файле PlaceBouquetOrderRequest.cs:

// PlaceBouquetOrderRequest.cs
public class PlaceBouquetOrderRequest : IRequest<Guid>
{
    public DateTime DueDate { get; init; }
    public int FlowersCount { get; init; }
    public string? Note { get; init; }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

и инициализировать обработчик рядом с ним:

// PlaceBouquetOrderRequest.cs
public class PlaceBouquetOrderRequestHandler : IRequestHandler<PlaceBouquetOrderRequest, Guid>
{
    public Task<Guid> Handle(PlaceBouquetOrderRequest request, CancellationToken cancellationToken)
    {
        var orderId = new Guid();

        // Send the order to the merchant

        return Task.FromResult(orderId);
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, создайте связанный контроллер в Controllers/BouquetController.cs:

// BouquetController.cs
[Route("api/[controller]")]
public class BouquetController : ControllerBase
{
    private readonly IMediator _mediator;

    public BouquetController(IMediator mediator)
        => _mediator = mediator;

    [HttpPost("order")]
    public async Task<IActionResult> PlaceBouquetOrder([FromBody] PlaceBouquetOrderRequest request)
    {
        var orderId = await _mediator.Send(request);
        return Ok(orderId);
    }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Добавление побочных эффектов

Наше приложение работает отлично, но теперь наш клиент хочет, чтобы мы также отправляли событие в календарь продавца, чтобы он мог иметь представление о его расписании.

Обратите внимание, что здесь мы не будем выполнять валидацию, так как это не является целью данного урока.

Давайте вернемся к PlaceBouquetOrderRequest.cs и добавим дополнительные изменения:

public class PlaceBouquetOrderRequestHandler : IRequestHandler<PlaceBouquetOrderRequest, Guid>
{
    public Task<Guid> Handle(PlaceBouquetOrderRequest request, CancellationToken cancellationToken)
    {
        var orderId = new Guid();

        // Send the order to the merchant
       SendReminderToCalendarAt(request.DueDate);

        return Task.FromResult(orderId);
    }

   private void SendReminderToCalendarAt(DateTime dueDate)
   {
       // Send a reminder to the merchant's calendar
   }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Проблема

К сожалению, есть пара проблем, которые вы можете обнаружить:

  • Наш PlaceBouquetOrderRequestHandler, который когда-то отвечал за размещение заказов букетов, теперь также отвечает за планирование напоминаний: его область применения выходит за рамки его первоначальной ответственности.
  • Логика SendReminder может быть повторно использована в другом месте, и для этого потребуется либо продублировать метод, либо извлечь его в специальный сервис. Однако создание службы может привести к тому, что объект изменит структуру кода, созданного вокруг обработчиков.

Решение

Если мы немного подумаем об этом, то запрошенное действие больше похоже на «сделать что-то, когда заказ был размещен», а не просто отправить напоминание.

К счастью, в MediatR есть такой объект для представления этих событий и их обработки, они называются Notifications.

Давайте создадим его для решения нашей проблемы!

В новом файле BouquetOrderPlacedEvent.cs создайте следующее событие:

// BouquetOrderPlacedEvent.cs
public class BouquetOrderPlacedEvent : INotification
{
    public Guid OrderId { get; init; }
    public DateTime DueDate { get; init; }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем создать обработчик события, способный обрабатывать подобные уведомления:

// BouquetOrderPlacedEvent.cs
public class BouquetOrderPlacedEventHandler : INotificationHandler<BouquetOrderPlacedEvent>
{
    public Task Handle(BouquetOrderPlacedEvent notification, CancellationToken cancellationToken)
    {
        SendReminderToCalendarAt(notification.DueDate);

        return Task.CompletedTask;
    }

    private void SendReminderToCalendarAt(DateTime dueDate)
    {
        // Send a reminder to the merchant's calendar
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

И заменим нашу прежнюю логику в обработчике на выброс этого события:

// PlaceBouquetOrderRequestHandler.cs
public class PlaceBouquetOrderRequestHandler : IRequestHandler<PlaceBouquetOrderRequest, Guid>
{
    private readonly IPublisher _publisher;

    public PlaceBouquetOrderRequestHandler(IPublisher publisher)
        => _publisher = publisher;

    public Task<Guid> Handle(PlaceBouquetOrderRequest request, CancellationToken cancellationToken)
    {
        var orderId = new Guid();

        // Send the order to the merchant

         _publisher.Publish(new BouquetOrderPlacedEvent
        {
            OrderId = orderId,
            DueDate = request.DueDate
        });

        return Task.FromResult(orderId);
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Идем дальше

Если мы планируем обрабатывать новые виды заказов, мы можем обобщить наше событие до OrderPlacedEvent, чтобы абстрагировать его от вида заказа:

// BouquetOrderPlacedEvent.cs
public abstract class OrderPlacedEvent : INotification
{
    public Guid OrderId { get; init; }
    public DateTime DueDate { get; init; }
}

public class BouquetOrderPlacedEvent : OrderPlacedEvent { }
Вход в полноэкранный режим Выход из полноэкранного режима

Затем мы можем сделать наш обработчик общим, чтобы он мог обрабатывать любое событие, производное от базового класса OrderPlacedEvent:

public class OrderPlacedEventHandler<TOrderPlacedEvent> : INotificationHandler<TOrderPlacedEvent>
    where TOrderPlacedEvent : OrderPlacedEvent
{
    public Task Handle(TOrderPlacedEvent notification, CancellationToken cancellationToken)
    {
        SendReminderToCalendarAt(notification.DueDate);

        return Task.CompletedTask;
    }

    private void SendReminderToCalendarAt(DateTime dueDate)
    {
        // Send a reminder to the merchant's calendar
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что если бы мы просто изменили определение нашего обработчика на public class OrderPlacedEventHandler : INotificationHandler<OrderPlacedEvent>, MediatR не смог бы правильно направить событие в наш обработчик. Подробнее об этом вы можете прочитать в этом выпуске.

Полезные советы

И вуаля, мы перенесли нашу логику в специальный обработчик, который впоследствии может обрабатывать заказы другого типа. Мы также сохранили логику нашего обработчика как можно ближе к его текущему сценарию использования и, возможно, уменьшили бы количество зависимостей обработчика, если бы действительно реализовали связь со сторонним календарным сервисом.

Оцените статью
Procodings.ru
Добавить комментарий