data.mdx.frontmatter.hero_image

MediatR – Wprowadzenie eventów do świata .NET Core

2018-04-13 | .NET | bd90

Coraz więcej aplikacji jest tworzonych w architekturze rozproszonej, opartej o systemy wymiany informacj jak RabbitMQ czy ZeroMQ. Wykorzystując .NET Core nie zawsze potrzebujemy stawiać nowych maszyn odpowiedzialnych za rozsyłanie informacji po systemie. W dzisiejszym artykule chciałbym pokazać jak za pomocą biblioteki MediatR stworzyć szynę komunikacyjną w pamięci aplikacji. Zapraszam!

Czym jest szyna komunikacyjna?

Jest to kawałek softu odpowiadający za przekazanie informacji z jednej części systemu do drugiej. Przykładami takich Message Broker-ów są właśnie ZeroMQ, RabbitMQ, Apache Kafka. Nawet w świecie chmur mamy takie rozwiązania w systemie SaaS. Wystarczy spojrzeć na takie usługi jak Azure Service Bus lub Amazon SQS.

Wszystkie wyżej wymienione usługi wymagają jednak uruchomienia ich jako oddzielnych aplikacji / procesów / kontenerów, lub subskrybcji w chmurze publicznej. To jednak wiąże się z kosztami. Rozwiązaniem oszczędzającym zasobność naszego portfela, a zarazem wygodnym w użytkowaniu, jest bilioteka MediatR. Pozwala na stworzenie takiej szyny wewnątrz naszej aplikacji.

Po co mi ta szyna?

Główną zaletą korzystania z bilioteki MediatR jest zmniejszona złożoność. No dobra... Dante, mamy nasze wspaniałe .NET Core MVC, gdzie jest podział na kontrolery, modele, widoki. Jak to jest że dokładając kolejne byty pozowli mi to na obniżenie złożoności projektu i późniejsze łatwieszje utrzymanie projektu.

Przecież jak to jest jedna aplikacja, która nie musi się komunikować z innymi serwisami, to ja mogę to sobie w kontrolerze obrobić... Ba! Nawet jak będzie jakiś powtarzalny element kodu, to sobie jakiegoś helperka lub utilsa pierdykne i to będzie takie KISS, DRY, superduper cód malina.

Nie.

Po prostu nie.

No dobra, wyjaśnię: brałem już udział w projektach, w których opieranie całej architektury na wzorcu MVC powodowało bardzo szybkie rozrastanie się kontrolerów do olbrzymich rozmiarów. Nie wygląda to dobrze. Zdecydowanie wolałbym mieć małe event-y. Cenie niezależność, tym bardziej, jeśli pozwala mi to przerobić implementacje innego handler-a nie wywalając 3 innych elementów.

Czy to jedyna korzyść? Taki ch... chyba nie sądzicie. Dodatkowym bonusem jest ułatwione przejście z aplikacji monolitycznej na architekturę mikroserwisów. Można powiedzieć, że w ten sposób jest to w ogóle wykonalne (co nie zmienia faktu, że i tak będzie wymagało sporego wkładu pracy).

Korzystanie z biblioteki MediatR

Rozpoczęcie korzystania z jej dobrodziejstw wymaga dodania dwóch paczek do projektu.

<PackageReference Include="MediatR" Version="4.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="4.0.0" />

Pierwsza paczka o nazwie MediatR zawiera cały Core. Druga zaś posiada MediatR.Extensions.Microsoft.DependencyInjection. Pomaga to poprawnie zarejestrować bibliotekę w kontenerze DI aplikacji .NET Core MVC. Dodatkowo paczka ta automatycznie rejestruje wszystkie Handler-y w naszym projekcie.

Najłatwiejszym sposobem dodania ich jest wykorzystanie metody "AddMediatR", która rozszerza interfejs IServiceCollection.

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddMediatR();
    // ...
}

Jeżeli nasze handlery znajdują się w innym assembly, to możemy użyć przeciążenie tej metody i po prostu wskazać mu, jaką assembly ma przeszukać.

Pierwszy Event

Eventami tutaj są proste obiekty POCO implementujące interfejs INotification.

public class MyFirstEvent : INotification
{
   public string Message { get; }

    public SendRegistractionEmailEvent(string message)
    {
        Message = message;
    }
}

Jest to tylko interfejs wskaźnikowy. Nie zmusza nas do implementacji żadnej specjalnej metody.

Następnie, przechodzimy do implementacji klasy, która będzie obsługiwała wyżej napisany Event. Klasa musi implementować INotificationHandler, gdzie jako parameter T musimy podać jaki event chcemy obsłużyć w tej klasie.

Interfejs wymusi na nas implementacje metody "public Task Handle(T notification, CancellationToken cancellationToken)". To właśnie w tej metodzie będzie nasza cała implementacja obsługi event-u. Stwórzmy sobie przykładową klasę "MyFirstHandler".

public class MyFirstHandler : INotificationHandler<MyFirstEvent>
{
    
    public Task Handle(MyFirstEvent notification, CancellationToken cancellationToken)
    {            
        Console.WriteLine(notification.Message);
    }
}

Wystarczy tylko dodać pobieranie z kontenera DI instancji mediator-a do kontrolera. Następnie, w jakiejś akcji kontrolera, możemy wysłać nasz event za pomocą metody Publish.

public MyController : Controller
{
    private readonly IMediator _mediator;  
 
    public MyController(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task MyAction()
    {
        // ...
        await _mediator.Publish(new MyFirstEvent("Test message"))
        // ...
    }
}

Gotowe! Mamy pełną implementację event-ów w C# i .NET Core.

Otrzymanie odpowiedzi po wysłaniu eventu

Za prosto nam idzie. Rozważmy więc pewien problem: co się stanie w przypadku, kiedy potrzebujemy otrzymać informację zwrotną z event-u? Np. by dowiedzieć się, czy dana operacją zakończyła się powodzeniem, lub w ramach wyświetlenia użytkownikowi komunikatu o wystąpieniu błędu. Takie zachowanie też jest przewidziane w bibliotece MediatoR.

W świecie MediatoR-a taki use case nazywamy "Request". To coś jak HTTP Request :), a przynajmniej widać podobieństwa w działaniu. Wysyłamy żądanie i otrzymujemy odpowiedź.

Wracając do sedna - teraz nasz obiekt POCO musi dziedziczyć po interfejsie IRequest. W tym wypadku T oznacza typ zwrotny, który ma zostać zwrócony przez handler.

public class SayHelloRequest : IRequest<string>
{
    public string Name { get; }

    public CreateNewAccountEvent(string name)
    {
        Name = name
    }
}

Teraz, tak samo jak w przypadku event-u, musimy zaimplementować klasę. Będzie odpowiedzialna za obsłużenie request-u SayHello. Tym razem ta klasa musi implementować interfejs IRequestHandler<Tin, Tout>, gdzie w Tin musimy podać klasę obsługiwanego request-u. Tout to zwracany, przez metodę Handle, typ danych. Interfejs zmusi nas do implementacji metody "public Task Handle(T notification, CancellationToken cancellationToken)". Nasza klasa będzie wyglądała następująco.

public class SayHelloHandler : IRequestHandler<SayHelloRequest, string>
{   
    public Task Handle(SayHelloRequest @event,
        CancellationToken cancellationToken)
    {
        return $"Hello {@event.Name}";
    }
}

Teraz wystarczy na instancji MediatR wywołać metodę Send i przekazać nowo stworzony obiekt SayHelloRequest. Metoda Send zwróci nam wynik wywołania metody Handle z naszego SayHelloHandler.

public MyController : Controller
{
    private readonly IMediator _mediator;  
 
    public MyController(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task MyAction()
    {
        // ...
        var result = await _mediator.Send(new SayHelloRequest("Dante"))
        // ...
    }
}

Podsumowanie

Tak oto w prosty sposób zrozumiały przez każdego śmiertelnika... Nie no dobra, nie jest to takie proste. Wierze w Was! Dlatego pokazuje jak łatwo możemy przejść z napchanych kontrolerów, serwisów, czy innych abstrakcyjnych bytów, do architektury event-owej. Należy pamiętać, że zarówno bardzo rozbudowane kontrolery jak i milion zdefiniowanych envet-ów, nie przysłuży się do stworzenia bardziej utrzymylwanej aplikacji. Wszystko trzeba stosować z głową :)

Dzięki za poświęcony czas poświęcony na przeczytanie artykułu. Liczę, że część wiedzy pozostała i uda się zastosować wszystko w praktyce.

Do Następnego!

Cześć

By Bd90 | 13-04-2018 | .NET