Menu Zamknij

Problem z publikacją eventów z poziomu repozytorium

Cześć, jako że aktualnie piszę aplikację na boku w której wykorzystuje CQRS i Event Sourcing chciałbym podzielić się z pewnym problemem który zabrał mi trochę czasu, zanim znalazłem rozwiązanie i zrozumiałem w czym tkwił problem. Może komuś innemu zaoszczędzi to trochę czasu, lub po prostu post ten będzie dobrą okazją by dowiedzieć się czegoś nowego.

Background sytuacji

W aplikacji, w jednym z Bounded Contextów skorzystałem z Event Sourcingu. Z tego też powodu przy każdej wywołanej na agregacie komendzie, chciałem zapisać wykonany na nim event bezpośrednio do bazy, aby w przyszłości istniała możliwość odtworzenia stanu aplikacji z dowolnego okresu z przeszłości. W tym celu w agregacie umieściłem listę IEnumerable<IEvent> w której trzymałem wszystkie eventy. Żeby zachować spójność danych między bazami Read i Write, postanowiłem że publikacja eventów do bazy Read odbędzie się dopiero po udanym commicie do bazy Write. Nie przypadkowo eventy traktujemy jako zdarzenia, po czymś co się już stało. Z tego powodu, nie chciałem ich rzucać z poziomu agregatu, gdyż nie nie miałbym zagwarantowanej pewności że commit do bazy się powiedzie. Dzięki temu mam pewność, że baza służąca do odczytu nie dostanie nieprawdziwych informacji. Dlatego w repozytorium, po zapisaniu zmian wywołuję prywatną metodę RaiseEvents(IEnumerable<IEvent> events) której implementacja wygląda dosyć prosto:

Jak widać przekazuję wszystkie eventy na szynę, która następnie zajmuje się przekazaniem ich do odpowiednich handlerów.

No i tutaj pojawił się mały problem

Początkowo implementacja metody Publish klasy EventBus wyglądała następująco:

Do zmiennej handlers przekazywaliśmy wszystkie rozwiązane zależności które implementują interfejs IEventHandler<TEvent> a następnie dla każdej z nich wywoływaliśmy metodę HandleAsync. Problemem o którym wcześniej nie pomyślałem, był fakt że w agregacie trzymaliśmy kolekcję IEvent przez co autofac próbował rozwiązać zależności dla typu bazowego, a nie dla tego który implementuje ten interfejs. Z tego powodu  liczba elementów w kolekcji zawsze wynosiła 0, a eventy nie były publikowane.

Rozwiązanie?

Okazało się to dla mnie nie lada problemem aby dobrać się do odpowiedniego typu, który pozwoliłby mi poprawnie rozwiązać zależności. Koniec końców kod który uzyskałem i który w pełni działa wygląda następująco:

Do zmiennej handlerType przypisujemy generyczny typ IEventHandler<>, implementujący docelowy typ naszego eventu. Z tego względu używamy metody MakeGenericType(), o której więcej możecie dowiedzieć się tu i tutaj. W ten sam sposób, w następnej linii uzyskujemy kolekcję naszych handlerów,. Dlaczego kolekcja? Ponieważ jeden event może mieć wiele implementacji, a zależy nam na tym by znaleźć je wszystkie. Bierze się to z tego faktu, że bardzo często chcemy poinformować inne agregaty o zmianie która zaszła w systemie, jak i zaktualizować nasze widoki służące odczytowi danych. Kolejnym krokiem jest rozwiązanie zależności dla naszej kolekcji event handlerów.

Ostatnią krokiem, jest dobranie się do metody HandleAsync którą implementuje IEventHandler<IEvent>

W tym miejscu musimy skorzystać z refleksji i do metody GetMethod() przekazać nazwę metody którą chcemy uzyskać z naszego EventHandlera, jak i typów parametrów które implementuje. W naszym wypadku będzie to @event.GetType(), ponieważ chcemy wywołać tą metodę z handlera która obsługuje interesujący nas typ eventu. Ostatnia linijka naszej magi to wywołanie wyżej znalezionej metody za pomocą Invoke() i przekazaniem do niej handlera, na którym chcemy ją wykonać, oraz parametrów które implementuje. Parametry przekazujemy jako tablicę obiektów, w naszym wypadku jest to jednoelementowa tablica, zawierająca nasz event. Dodatkowo wszystko rzutujemy na typ Task aby zachować dalszą asynchroniczność wywołań.

Podsumowanie

Już parę razy miałem przyjemność w swoich prywatnych projektach, używać CQRS’a i nie wydawał mi się specjalnie trudny w implementacji. Jak widać życie pisze różne scenariusze i pokazuje że gdy wydaje nam się że wiemy dużo, tak naprawdę nie wiemy nic 😛 Dla mnie to fajna lekcja, pokazująca że czasem naprawdę trzeba pokombinować aby uzyskać dosyć prosty efekt, jak widzimy powyżej. Nie wiem czy jest to najbardziej optymalna metoda oraz czy można to zrobić lepiej. Tak naprawdę to jedyne rozwiązanie które u mnie zadziałało, ale jeżeli spotkaliście się z podobnym problemem i macie lepsze rozwiązanie, to bardzo chętnie dowiem się w jaki sposób to zrobiliście. Mam też nadzieję że ten kawałek kodu, pozwoli komuś zaoszczędzić trochę czasu, lub chociaż poszerzyć swoją wiedzę o takie przypadki 🙂

6 Komentarzy

    • contend

      Hej,
      faktycznie, masz rację. Korzystając z Task.WhenAll odpalimy wszystkie handlery równolegle, zamiast czekać na wykonanie każdego z osobna. Dzięki wielkie za zwrócenie na to uwagi 🙂

  1. Marcin

    Czy metody:

    var handlerMethod = handler.GetType().GetMethod(„HandleAsync”, new[] {@event.GetType()});
    await (Task) ((dynamic) handlerMethod.Invoke(handler, new object[] {@event}));

    nie można przypadkiem uprościć do:

    await (Task) ((dynamic)handler).HandleAsync(@event);

    ?

    • contend

      Sprawdziłem sobie na szybko twoją propozycję i niestety VisualStudio rzuca następującym wyjątkiem:
      Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: ‚The best overloaded method match for ‚SomeEventHandler.HandleAsync(SomeEvent)’ has some invalid arguments’
      Postaram się jeszcze w miarę możliwości sprawdzić dlaczego tak się dzieje.

        • contend

          Faktycznie, teraz wszystko śmiga, ale postaram się zrezygnować z używania dynamic w tym miejscu. Mam pewien pomysł, postaram sie go wprowadzić i ewentualnie zedytuje ten post.
          Mimo wszystko, dzięki za zaangażowanie, bo naprawdę dobry pomysł jak to prościej rozwiązać 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *