Problem z publikacją eventów z poziomu repozytorium

Mateusz Gajda10/29/2018 - 3 min read

Photo by: Chris Liverani

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:

private async Task RaiseEvents(IEnumerable<*IEvent*> events)
{
foreach (var @event in events)
{
await _eventBus.Publish(@event);
}
}

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:

public async Task Publish<*TEvent*>(TEvent @event) where TEvent : IEvent
{
var handlers = _context.Resolve<*IEnumerable*<*IEventHandler*<*TEvent*>*>*>().ToList();
foreach (var handler in handlers)
{
await handler.HandleAsync(@event);
}
}

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:

public async Task Publish<*TEvent*>(TEvent @event) where TEvent : IEvent
{
var handlerType = typeof(IEventHandler*<*>).MakeGenericType(@event.GetType());
var handlerCollectionType = typeof(IEnumerable*<*>).MakeGenericType(handlerType);
var eventHandlers = _context.Resolve(handlerCollectionType);
var handlers = new List<*Task*>();
foreach (var handler in (IEnumerable) eventHandlers)
{
var handlerMethod = handler.GetType().GetMethod(HandleAsync, new[] {@event.GetType()});
handlers.Add((Task) (handlerMethod.Invoke(handler, new object[] {@event})));
}
await Task.WhenAll(handlers);
}

Do zmiennej handlerType przypisujemy generyczny typ IEventHandler<in T>, 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>

public interface IEventHandler<*in TEvent*> : IEventHandler where TEvent : IEvent
{
Task HandleAsync(TEvent @event);
}

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 :P 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 :)