Own mediator
![Own mediator](/content/images/size/w2000/2024/06/Mi-o---Blog-Cover---k-pia.png)
V súčasnosti sa často používa návrhový vzor Mediator. Jeho cieľom je oddeliť komunikáciu medzi objektami od seba navzájom. Čo zníži závislosti medzi triedami, sprehľadní kód a zjednoduší testovanie.
V .NET prostredí vývojár často siahne po super knižnici MediatR od Jimmyho Bogarda. Táto knižnica poskytuje jednoduchý spôsob ako implementovať Mediator do aplikácie.
Sú však situácie kedy nemôžeme / nechceme využiť knižnicu tretích strán. Napríklad píšete nejakú vlastnú knižnicu / framework a nechcete do nej zavádzať takéto závislosti.
Niečo podobné sa stalo aj nám. Našťastie sme nepotrebovali komplet funkčnosť akú poskytuje MediatR, ale stačilo nám len základné odoslanie udalosti a jej spracovanie.
Preto teraz ukážem ako si jednoducho vytvoriť takúto implementáciu vlastného Mediatoru.
Začnime s definovaním rozhrania správy. V našom prípade sa jednalo o doménovú udalosť, preto ten názov.
public interface IDomainEvent
{
}
Je to len čisto značkovacie rozhranie. Zaobišli by sme sa aj bez neho, ale je to dobrý spôsob ako označiť, že ide o udalosť.
Následne rozhranie pre spracovanie udalostí.
public interface IDomainEventHandler<TEvent>
where TEvent : IDomainEvent
{
Task HandleAsync(TEvent domainEvent, CancellationToken cancellationToken = default);
}
Potrebujeme ešte rozhranie pre odosielanie udalostí.
public interface IEventPublisher
{
Task PublishAsync<TEvent>(TEvent domainEvent, CancellationToken cancellationToken = default)
where TEvent : IDomainEvent;
}
Tu by sme s abstrakciami mohli skončiť a začať implementovať. My sme sa však rozhodli pridať tam ešte abstrakciu pre stratégiu spracovania.
Nateraz nám stačilo postupné spracovanie udalostí, ale v budúcnosti by sme mohli potrebovať aj iné stratégie.
public interface IEventPublisherStrategy
{
Task PublishAsync<TEvent>(
IEnumerable<IDomainEventHandler<TEvent>> handlers,
TEvent domainEvent,
CancellationToken cancellationToken)
where TEvent : IDomainEvent;
}
Implementácia
Publisher môže vyzerať napríklad takto:
internal class EventPublisher : IEventPublisher
{
private readonly IServiceProvider _serviceProvider;
private readonly IEventPublisherStrategy _publisherStrategy;
private readonly IHttpContextAccessor _httpContextAccessor;
public EventPublisher(
IServiceProvider serviceProvider,
IHttpContextAccessor httpContextAccessor,
IEventPublisherStrategy publisherStrategy)
{
_serviceProvider = serviceProvider;
_publisherStrategy = publisherStrategy;
_httpContextAccessor = httpContextAccessor;
}
public async Task PublishAsync<TEvent>(TEvent domainEvent, CancellationToken cancellationToken = default)
where TEvent : IDomainEvent
{
IEnumerable<IDomainEventHandler<TEvent>> handlers;
if (_httpContextAccessor.HttpContext is not null)
{
// 👇 if available, then find handlers in HttpContext services
handlers = GetHandlers<TEvent>(_httpContextAccessor.HttpContext.RequestServices);
// 👇 publish event to all handlers
await _publisherStrategy.PublishAsync(handlers, domainEvent, cancellationToken);
}
else
{
// 👇 if not, then create new scope
using var scope = _serviceProvider.CreateScope();
handlers = GetHandlers<TEvent>(scope.ServiceProvider);
// 👇 publish event to all handlers
await _publisherStrategy.PublishAsync(handlers, domainEvent, cancellationToken);
}
}
private static IEnumerable<IDomainEventHandler<TEvent>> GetHandlers<TEvent>(IServiceProvider serviceProvider)
where TEvent : IDomainEvent
=> serviceProvider.GetServices<IDomainEventHandler<TEvent>>();
}
Publisher sa stará o získanie handlerov z kontajnera a následne ich prepošle na publisher strategy, ktorá rozhodne o tom ako budú spracované.
public sealed class ForeachAwaitPublisherStrategy : IEventPublisherStrategy
{
public async Task PublishAsync<TEvent>(
IEnumerable<IDomainEventHandler<TEvent>> handlers,
TEvent domainEvent,
CancellationToken cancellationToken)
where TEvent : IDomainEvent
{
// 👇 publish event to all handlers
foreach (var handler in handlers)
{
await handler.HandleAsync(domainEvent, cancellationToken);
}
}
}
Náš ForeachAwaitPublisherStrategy
spracuje udalosti postupne. Správu posunie všetkým handlerom a počká na ich spracovanie.
Registrácia
Aby sme si mohli vytvorené abstrakcie použiť, musíme ich zaregistrovať v DI kontajneri.
public static IServiceCollection AddEventPublisher(this IServiceCollection services)
{
services.AddHttpContextAccessor();
services.TryAddSingleton<IEventPublisher, EventPublisher>();
services.TryAddSingleton<IEventPublisherStrategy, ForeachAwaitPublisherStrategy>();
return services;
}
Rovnako si môžeme vytvoriť extension aj na jednoduché zaregistrovanie handlerov.
public static IServiceCollection AddDomainEventHandler<TEvent, THandler>(
this IServiceCollection services,
ServiceLifetime lifetime = ServiceLifetime.Transient)
where TEvent : IDomainEvent
where THandler : class, IDomainEventHandler<TEvent>
{
services.Add(new ServiceDescriptor(typeof(IDomainEventHandler<TEvent>), typeof(THandler), lifetime));
return services;
}
Použitie tejto extension vyzerá nasledovne
services.AddDomainEventHandler<YourEvent, YourHandler>();
.
Je to z dôvodu, že som sa chcel vyhnúť reflexii. Pokiaľ ti reflexia neprekáža, tak sa dá dosiahnúť aj zjednodušené volanieservices.AddDomainEventHandler<YourHandler>();
.
To ale nechám na teba 😉.
Použitie
Nakoniec si ukážeme ako použiť náš vlastný Mediator.
Udalosť a handler:
// 👇 The event that the product was created
public record ProductCreated(int Id, string Name, decimal Price) : IDomainEvent;
// 👇 The handler that will process the event
public class ProductCreatedHandler : IDomainEventHandler<ProductCreated>
{
public Task HandleAsync(ProductCreated domainEvent, CancellationToken cancellationToken = default)
{
// 👇 Process the event
Console.WriteLine($"Product created: {domainEvent.Name}");
return Task.CompletedTask;
}
}
A registrácia:
builder.Services.AddEventPublisher();
builder.Services.AddDomainEventHandler<ProductCreated, ProductCreatedHandler>();
Použitie v endpointe:
app.MapPost("/products", async (Product product, IEventPublisher publisher) =>
{
// Save the product
// 👇 Publish the event
await publisher.PublishAsync(new ProductCreated(product.Id, product.Name, product.Price));
return product;
});