При реализации бизнес-логики внутри обработчика вы можете захотеть выполнить некоторые действия в качестве побочного эффекта обработки.
Наличие этих действий в обработчике может привести к раздуванию кода или дублированию, если вы захотите выполнить некоторые из них в нескольких обработчиках.
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 не смог бы правильно направить событие в наш обработчик. Подробнее об этом вы можете прочитать в этом выпуске.
Полезные советы
И вуаля, мы перенесли нашу логику в специальный обработчик, который впоследствии может обрабатывать заказы другого типа. Мы также сохранили логику нашего обработчика как можно ближе к его текущему сценарию использования и, возможно, уменьшили бы количество зависимостей обработчика, если бы действительно реализовали связь со сторонним календарным сервисом.