Zdarzenia domeny: projektowanie i implementacja

Napiwek

Ta zawartość jest fragmentem książki eBook, architektury mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET dostępnych na platformie .NET Docs lub jako bezpłatnego pliku PDF, który można odczytać w trybie offline.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Użyj zdarzeń domeny, aby jawnie zaimplementować skutki uboczne zmian w domenie. Innymi słowy, i używając terminologii DDD, użyj zdarzeń domeny do jawnego zaimplementowania efektów ubocznych w wielu agregacjach. Opcjonalnie, aby uzyskać lepszą skalowalność i mniejszy wpływ na blokady bazy danych, użyj spójności ostatecznej między agregacjami w tej samej domenie.

Co to jest zdarzenie domeny?

Zdarzenie jest czymś, co wydarzyło się w przeszłości. Zdarzenie domeny to coś, co wydarzyło się w domenie, o której chcesz wiedzieć inne części tej samej domeny (w procesie). Powiadomienia zwykle reagują na zdarzenia.

Ważną zaletą zdarzeń domeny jest to, że skutki uboczne można wyrazić jawnie.

Jeśli na przykład korzystasz tylko z platformy Entity Framework i musisz zareagować na jakieś zdarzenie, prawdopodobnie koduje to, czego potrzebujesz blisko tego, co wyzwala zdarzenie. Dlatego reguła jest połączona niejawnie z kodem i musisz przyjrzeć się kodowi, miejmy nadzieję, zdać sobie sprawę, że reguła jest tam zaimplementowana.

Z drugiej strony użycie zdarzeń domeny sprawia, że koncepcja jest DomainEvent jawna, ponieważ istnieje co najmniej jedna DomainEventHandler z nich.

Na przykład w aplikacji eShop po utworzeniu zamówienia użytkownik staje się nabywcą, więc element OrderStartedDomainEvent jest zgłaszany i obsługiwany w ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandlerobiekcie , więc podstawowa koncepcja jest widoczna.

Krótko mówiąc, zdarzenia domeny pomagają wyrazić, jawnie, reguły domeny oparte na wszechobecnym języku dostarczonym przez ekspertów domeny. Zdarzenia domeny umożliwiają również lepsze rozdzielenie problemów między klasami w tej samej domenie.

Ważne jest, aby upewnić się, że wszystkie operacje związane ze zdarzeniem domeny zakończą się pomyślnie lub żadne z nich nie są wykonywane, podobnie jak transakcja bazy danych.

Zdarzenia domeny są podobne do zdarzeń w stylu obsługi komunikatów, z jedną ważną różnicą. W przypadku rzeczywistych komunikatów, kolejkowania komunikatów, brokerów komunikatów lub magistrali usług przy użyciu protokołu AMQP komunikat jest zawsze wysyłany asynchronicznie i przekazywany między procesami i maszynami. Jest to przydatne w przypadku integrowania wielu kontekstów ograniczonych, mikrousług, a nawet różnych aplikacji. Jednak w przypadku zdarzeń domeny chcesz zgłosić zdarzenie z aktualnie uruchomionej operacji domeny, ale chcesz, aby wszelkie skutki uboczne miały miejsce w tej samej domenie.

Zdarzenia domeny i ich skutki uboczne (akcje wyzwalane później, które są zarządzane przez programy obsługi zdarzeń) powinny występować niemal natychmiast, zwykle w trakcie procesu i w tej samej domenie. W związku z tym zdarzenia domeny mogą być synchroniczne lub asynchroniczne. Jednak zdarzenia integracji powinny być zawsze asynchroniczne.

Zdarzenia domeny a zdarzenia integracji

Zdarzenia domeny i integracji są takie same: powiadomienia o tym, co właśnie się stało. Jednak ich implementacja musi być inna. Zdarzenia domeny to tylko komunikaty wypychane do dyspozytora zdarzeń domeny, które można zaimplementować jako mediator w pamięci na podstawie kontenera IoC lub dowolnej innej metody.

Z drugiej strony celem zdarzeń integracji jest propagowanie zatwierdzonych transakcji i aktualizacji do dodatkowych podsystemów, niezależnie od tego, czy są to inne mikrousługi, ograniczone konteksty, czy nawet aplikacje zewnętrzne. W związku z tym powinny wystąpić tylko wtedy, gdy jednostka została pomyślnie utrwalone, w przeciwnym razie jest tak, jakby cała operacja nigdy nie miała miejsce.

Jak wspomniano wcześniej, zdarzenia integracji muszą być oparte na asynchronicznej komunikacji między wieloma mikrousługami (innymi powiązanymi kontekstami), a nawet zewnętrznymi systemami/aplikacjami.

W związku z tym interfejs magistrali zdarzeń wymaga pewnej infrastruktury, która umożliwia międzyprocesową i rozproszoną komunikację między potencjalnie zdalnymi usługami. Może on być oparty na komercyjnej magistrali usług, kolejkach, udostępnionej bazie danych używanej jako skrzynka pocztowa lub innych rozproszonych i idealnie wypychanych systemach obsługi komunikatów.

Zdarzenia domeny jako preferowany sposób wyzwalania skutków ubocznych w wielu agregacjach w tej samej domenie

Jeśli wykonanie polecenia związanego z jednym zagregowanym wystąpieniem wymaga uruchomienia dodatkowych reguł domeny w co najmniej jednej dodatkowej agregacji, należy zaprojektować i zaimplementować te skutki uboczne, które mają być wyzwalane przez zdarzenia domeny. Jak pokazano na rysunku 7–14 i jako jeden z najważniejszych przypadków użycia, zdarzenie domeny powinno służyć do propagowania zmian stanu w wielu agregacjach w ramach tego samego modelu domeny.

Diagram showing a domain event controlling data to a Buyer aggregate.

Rysunek 7–14. Zdarzenia domeny w celu wymuszania spójności między wieloma agregacjami w tej samej domenie

Rysunek 7–14 pokazuje, jak spójność między agregacjami jest osiągana przez zdarzenia domeny. Gdy użytkownik inicjuje zamówienie, agregacja zamówień wysyła OrderStarted zdarzenie domeny. Zdarzenie domeny OrderStarted jest obsługiwane przez Agregację nabywcy w celu utworzenia obiektu Nabywca w mikrousłudze zamawiania na podstawie oryginalnych informacji użytkownika z mikrousługi tożsamości (z informacjami podanymi w poleceniu CreateOrder).

Alternatywnie można mieć zagregowany katalog główny subskrybowany dla zdarzeń zgłaszanych przez członków jego agregacji (jednostek podrzędnych). Na przykład każda jednostka podrzędna OrderItem może zgłosić zdarzenie, gdy cena produktu jest wyższa niż określona kwota lub gdy kwota produktu jest zbyt wysoka. Zagregowany katalog główny może następnie odbierać te zdarzenia i wykonywać globalne obliczenia lub agregację.

Należy pamiętać, że ta komunikacja oparta na zdarzeniach nie jest implementowana bezpośrednio w ramach agregacji; należy zaimplementować programy obsługi zdarzeń domeny.

Obsługa zdarzeń domeny jest problemem aplikacji. Warstwa modelu domeny powinna skupiać się tylko na logice domeny — elementy, które zna ekspert domeny, a nie infrastrukturę aplikacji, takie jak procedury obsługi i akcje trwałości efektów ubocznych przy użyciu repozytoriów. W związku z tym poziom warstwy aplikacji to miejsce, w którym powinny znajdować się programy obsługi zdarzeń domeny wyzwalające akcje po wystąpieniu zdarzenia domeny.

Zdarzenia domeny mogą być również używane do wyzwalania dowolnej liczby akcji aplikacji i co ważniejsze, muszą być otwarte, aby zwiększyć liczbę w przyszłości w sposób oddzielony. Na przykład po uruchomieniu zamówienia możesz opublikować zdarzenie domeny, aby propagować te informacje do innych agregacji, a nawet w celu podniesienia akcji aplikacji, takich jak powiadomienia.

Kluczowym punktem jest liczba otwartych akcji, które mają być wykonywane po wystąpieniu zdarzenia domeny. Ostatecznie akcje i reguły w domenie i aplikacji będą rosnąć. Złożoność lub liczba akcji ubocznych, gdy coś się stanie, ale jeśli kod został sprzężony z "klejem" (czyli tworzenie określonych obiektów za pomocą new), za każdym razem, gdy trzeba będzie dodać nową akcję, musisz również zmienić działający i przetestowany kod.

Ta zmiana może spowodować nowe błędy, a takie podejście jest również sprzeczne z zasadą Open/Closed z języka SOLID. Nie tylko, oryginalna klasa, która orkiestrowała operacje, rosłaby i rosła, co jest sprzeczne z zasadą odpowiedzialności pojedynczej (SRP).

Z drugiej strony, jeśli używasz zdarzeń domeny, możesz utworzyć szczegółową i oddzieloną implementację, oddzielając obowiązki przy użyciu tego podejścia:

  1. Wyślij polecenie (na przykład CreateOrder).
  2. Odbierz polecenie w procedurze obsługi poleceń.
    • Wykonaj transakcję pojedynczej agregacji.
    • (Opcjonalnie) Wywoływanie zdarzeń domeny dla skutków ubocznych (na przykład OrderStartedDomainEvent).
  3. Obsługa zdarzeń domeny (w ramach bieżącego procesu), które będą wykonywać otwartą liczbę skutków ubocznych w wielu agregacjach lub akcjach aplikacji. Na przykład: .
    • Zweryfikuj lub utwórz nabywcę i formę płatności.
    • Utwórz i wyślij powiązane zdarzenie integracji do magistrali zdarzeń, aby propagować stany między mikrousługami lub wyzwalać akcje zewnętrzne, takie jak wysyłanie wiadomości e-mail do kupującego.
    • Obsługa innych skutków ubocznych.

Jak pokazano na rysunku 7–15, począwszy od tego samego zdarzenia domeny, można obsługiwać wiele akcji związanych z innymi agregacjami w domenie lub dodatkowych akcji aplikacji, które należy wykonać w ramach mikrousług łączących się ze zdarzeniami integracji i magistralą zdarzeń.

Diagram showing a domain event passing data to several event handlers.

Rysunek 7–15. Obsługa wielu akcji na domenę

W warstwie aplikacji może istnieć kilka programów obsługi dla tego samego zdarzenia domeny. Jeden program obsługi może rozwiązać spójność między agregacjami a innym programem obsługi może opublikować zdarzenie integracji, dzięki czemu inne mikrousługi mogą z nim coś zrobić. Programy obsługi zdarzeń są zwykle w warstwie aplikacji, ponieważ obiekty infrastruktury, takie jak repozytoria lub interfejs API aplikacji, są używane do zachowania mikrousługi. W tym sensie programy obsługi zdarzeń są podobne do programów obsługi poleceń, więc obie są częścią warstwy aplikacji. Ważną różnicą jest to, że polecenie powinno być przetwarzane tylko raz. Zdarzenie domeny może być przetwarzane zero lub n razy, ponieważ może być odbierane przez wiele odbiorników lub procedur obsługi zdarzeń z innym celem dla każdego programu obsługi.

Posiadanie otwartej liczby procedur obsługi na zdarzenie domeny pozwala dodać dowolną liczbę reguł domeny zgodnie z potrzebami bez wpływu na bieżący kod. Na przykład zaimplementowanie następującej reguły biznesowej może być tak proste, jak dodanie kilku procedur obsługi zdarzeń (a nawet jednego):

Gdy łączna kwota zakupiona przez klienta w sklepie, w dowolnej liczbie zamówień, przekracza 6000 USD, zastosuj rabat 10% od rabatu na każde nowe zamówienie i powiadomi klienta pocztą e-mail o tej rabatie w przypadku przyszłych zamówień.

Implementowanie zdarzeń domeny

W języku C# zdarzenie domeny jest po prostu strukturą lub klasą gospodarstwa danych, na przykład obiektem DTO, ze wszystkimi informacjami dotyczącymi tego, co właśnie się stało w domenie, jak pokazano w poniższym przykładzie:

public class OrderStartedDomainEvent : INotification
{
    public string UserId { get; }
    public string UserName { get; }
    public int CardTypeId { get; }
    public string CardNumber { get; }
    public string CardSecurityNumber { get; }
    public string CardHolderName { get; }
    public DateTime CardExpiration { get; }
    public Order Order { get; }

    public OrderStartedDomainEvent(Order order, string userId, string userName,
                                   int cardTypeId, string cardNumber,
                                   string cardSecurityNumber, string cardHolderName,
                                   DateTime cardExpiration)
    {
        Order = order;
        UserId = userId;
        UserName = userName;
        CardTypeId = cardTypeId;
        CardNumber = cardNumber;
        CardSecurityNumber = cardSecurityNumber;
        CardHolderName = cardHolderName;
        CardExpiration = cardExpiration;
    }
}

Jest to zasadniczo klasa, która przechowuje wszystkie dane związane ze zdarzeniem OrderStarted.

Jeśli chodzi o wszechobecny język domeny, ponieważ zdarzenie jest czymś, co wydarzyło się w przeszłości, nazwa klasy zdarzenia powinna być reprezentowana jako czasownik z przeszłości, taki jak OrderStartedDomainEvent lub OrderShippedDomainEvent. W ten sposób zdarzenie domeny jest implementowane w mikrousłudze zamawiania w eShop.

Jak wspomniano wcześniej, ważną cechą zdarzeń jest to, że ponieważ zdarzenie jest czymś, co wydarzyło się w przeszłości, nie powinno się zmieniać. W związku z tym musi być niezmienną klasą. W poprzednim kodzie widać, że właściwości są tylko do odczytu. Nie ma możliwości zaktualizowania obiektu. Można ustawić tylko wartości podczas jego tworzenia.

Należy podkreślić tutaj, że jeśli zdarzenia domeny miały być obsługiwane asynchronicznie, przy użyciu kolejki, która wymagała serializacji i deserializacji obiektów zdarzeń, właściwości musiałyby być "zestawem prywatnym" zamiast tylko do odczytu, więc deserializator będzie mógł przypisać wartości podczas kolejkowania. Nie jest to problem w mikrousługa zamawiania, ponieważ pub/sub zdarzenia domeny jest implementowany synchronicznie przy użyciu usługi MediatR.

Wywoływanie zdarzeń domeny

Następnym pytaniem jest, jak zgłosić zdarzenie domeny, aby dotrzeć do powiązanych procedur obsługi zdarzeń. Można użyć wielu podejść.

Udi Dahan pierwotnie zaproponował (na przykład w kilku powiązanych wpisach, takich jak Zdarzenia domeny – Take 2) przy użyciu klasy statycznej do zarządzania zdarzeniami i podnoszenia ich. Może to obejmować klasę statyczną o nazwie DomainEvents, która będzie zgłaszać zdarzenia domeny natychmiast po wywołaniu przy użyciu składni, takiej jak DomainEvents.Raise(Event myEvent). Jimmy Bogard napisał wpis w blogu (Wzmocnienie domeny: Zdarzenia domeny), który zaleca podobne podejście.

Jednak gdy klasa zdarzeń domeny jest statyczna, jest również wysyłana do programów obsługi natychmiast. Sprawia to, że testowanie i debugowanie jest trudniejsze, ponieważ programy obsługi zdarzeń z logiką skutków ubocznych są wykonywane natychmiast po wywołaniu zdarzenia. Podczas testowania i debugowania po prostu chcesz skupić się na tym, co dzieje się w bieżących klasach agregujących; Nie chcesz nagle przekierowywać do innych programów obsługi zdarzeń dla efektów ubocznych związanych z innymi agregacjami lub logiką aplikacji. Dlatego inne podejścia ewoluowały, jak wyjaśniono w następnej sekcji.

Odroczone podejście do zgłaszania i wysyłania zdarzeń

Zamiast wysyłać do programu obsługi zdarzeń domeny natychmiast, lepszym rozwiązaniem jest dodanie zdarzeń domeny do kolekcji, a następnie wysłanie tych zdarzeń domeny bezpośrednio przed lub bezpośredniopo zatwierdzeniu transakcji (tak jak w przypadku funkcji SaveChanges w programie EF). (To podejście zostało opisane przez Jimmy Bogard w tym poście Lepszy wzorzec zdarzeń domeny).

Podjęcie decyzji o wysłaniu zdarzeń domeny bezpośrednio przed lub bezpośrednio po zatwierdzeniu transakcji jest ważne, ponieważ określa, czy zostaną uwzględnione skutki uboczne w ramach tej samej transakcji, czy w różnych transakcjach. W tym ostatnim przypadku należy radzić sobie ze spójnością ostateczną w wielu agregacjach. Ten temat został omówiony w następnej sekcji.

Odroczone podejście jest używane przez eShop. Najpierw dodasz zdarzenia wykonywane w jednostkach do kolekcji lub listy zdarzeń na jednostkę. Ta lista powinna być częścią obiektu jednostki, a nawet lepiej, częścią klasy jednostki bazowej, jak pokazano w poniższym przykładzie klasy bazowej jednostki:

public abstract class Entity
{
     //...
     private List<INotification> _domainEvents;
     public List<INotification> DomainEvents => _domainEvents;

     public void AddDomainEvent(INotification eventItem)
     {
         _domainEvents = _domainEvents ?? new List<INotification>();
         _domainEvents.Add(eventItem);
     }

     public void RemoveDomainEvent(INotification eventItem)
     {
         _domainEvents?.Remove(eventItem);
     }
     //... Additional code
}

Jeśli chcesz zgłosić zdarzenie, wystarczy dodać je do kolekcji zdarzeń z kodu w dowolnej metodzie jednostki agregującej głównej.

Poniższy kod, część elementu Order aggregate-root w eShop, przedstawia przykład:

var orderStartedDomainEvent = new OrderStartedDomainEvent(this, //Order object
                                                          cardTypeId, cardNumber,
                                                          cardSecurityNumber,
                                                          cardHolderName,
                                                          cardExpiration);
this.AddDomainEvent(orderStartedDomainEvent);

Zwróć uwagę, że jedyną rzeczą, którą robi metoda AddDomainEvent, jest dodanie zdarzenia do listy. Żadne zdarzenie nie jest jeszcze wysyłane i nie jest jeszcze wywoływana żadna procedura obsługi zdarzeń.

W rzeczywistości chcesz wysłać zdarzenia później po zatwierdzeniu transakcji do bazy danych. Jeśli używasz programu Entity Framework Core, oznacza to, że w metodzie SaveChanges środowiska EF DbContext, jak w poniższym kodzie:

// EF Core DbContext
public class OrderingContext : DbContext, IUnitOfWork
{
    // ...
    public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        // Dispatch Domain Events collection.
        // Choices:
        // A) Right BEFORE committing data (EF SaveChanges) into the DB. This makes
        // a single transaction including side effects from the domain event
        // handlers that are using the same DbContext with Scope lifetime
        // B) Right AFTER committing data (EF SaveChanges) into the DB. This makes
        // multiple transactions. You will need to handle eventual consistency and
        // compensatory actions in case of failures.
        await _mediator.DispatchDomainEventsAsync(this);

        // After this line runs, all the changes (from the Command Handler and Domain
        // event handlers) performed through the DbContext will be committed
        var result = await base.SaveChangesAsync();
    }
}

Za pomocą tego kodu należy wysłać zdarzenia jednostki do odpowiednich programów obsługi zdarzeń.

Ogólny wynik polega na tym, że zebranie zdarzenia domeny (proste dodanie do listy w pamięci) zostało oddzielone od wysyłania go do programu obsługi zdarzeń. Ponadto w zależności od rodzaju używanego dyspozytora można wysyłać zdarzenia synchronicznie lub asynchronicznie.

Należy pamiętać, że granice transakcyjne mają tu znaczący wpływ. Jeśli jednostka pracy i transakcji może obejmować więcej niż jedną agregację (tak jak w przypadku korzystania z programu EF Core i relacyjnej bazy danych), może to działać dobrze. Jeśli jednak transakcja nie może obejmować agregacji, musisz zaimplementować dodatkowe kroki w celu osiągnięcia spójności. Jest to kolejny powód, dla którego ignorancja trwałości nie jest uniwersalna; zależy to od używanego systemu magazynowania.

Pojedyncza transakcja w agregacjach a spójność ostateczna w agregacjach

Pytanie, czy wykonać jedną transakcję w agregacjach, a nie polegać na spójności ostatecznej w tych agregacjach, jest kontrowersyjna. Wielu autorów DDD, takich jak Eric Evans i Vaughn Vernon opowiada się za zasadą, że jedna transakcja = jedna agregacja, a zatem argumentuje za ostateczną spójnością w agregacjach. Na przykład w swojej książce Domain-Driven Design Eric Evans mówi:

Każda reguła, która obejmuje agregacje, nie będzie aktualizowana przez cały czas. Za pomocą przetwarzania zdarzeń, przetwarzania wsadowego lub innych mechanizmów aktualizacji inne zależności można rozwiązać w określonym czasie. (strona 128)

Vaughn Vernon mówi następujące w Effective Aggregate Design. Część II: Łączenie agregacji:

W związku z tym, jeśli wykonanie polecenia na jednym zagregowanym wystąpieniu wymaga, aby dodatkowe reguły biznesowe wykonywane na co najmniej jednej agregacji, użyj spójności ostatecznej [...] Istnieje praktyczny sposób obsługi spójności ostatecznej w modelu DDD. Metoda agregacji publikuje zdarzenie domeny, które jest w czasie dostarczane do co najmniej jednego asynchronicznego subskrybenta.

To uzasadnienie opiera się na objęciu precyzyjnych transakcji zamiast transakcji obejmujących wiele agregacji lub jednostek. Chodzi o to, że w drugim przypadku liczba blokad bazy danych będzie znacząca w aplikacjach na dużą skalę z wysokimi potrzebami skalowalności. Uwzględnienie faktu, że wysoce skalowalne aplikacje nie muszą mieć natychmiastowej spójności transakcyjnej między wieloma agregacjami, pomaga zaakceptować koncepcję spójności ostatecznej. Zmiany niepodzielne często nie są wymagane przez firmę i w każdym razie odpowiedzialność ekspertów ds. domeny powiedzieć, czy konkretne operacje wymagają transakcji niepodzielnych, czy nie. Jeśli operacja zawsze wymaga niepodzielnej transakcji między wieloma agregacjami, możesz zapytać, czy agregacja powinna być większa, czy nie została prawidłowo zaprojektowana.

Jednak inni deweloperzy i architekci, tacy jak Jimmy Bogard, są w porządku z połączeniem jednej transakcji w kilku agregacjach — ale tylko wtedy, gdy te dodatkowe agregacje są związane z efektami ubocznymi dla tego samego oryginalnego polecenia. Na przykład w wzorcu lepszych zdarzeń domeny Bogard mówi:

Zazwyczaj chcę, aby skutki uboczne zdarzenia domeny miały miejsce w ramach tej samej transakcji logicznej, ale niekoniecznie w tym samym zakresie podnoszenia zdarzenia domeny [...] Tuż przed zatwierdzeniem transakcji wysyłamy nasze zdarzenia do odpowiednich programów obsługi.

Jeśli wysyłasz zdarzenia domeny bezpośrednio przed zatwierdzeniem oryginalnej transakcji, jest to spowodowane tym, że skutki uboczne tych zdarzeń mają być uwzględnione w tej samej transakcji. Jeśli na przykład metoda EF DbContext SaveChanges zakończy się niepowodzeniem, transakcja wycofa wszystkie zmiany, w tym wynik wszelkich operacji efektów ubocznych wdrożonych przez powiązane procedury obsługi zdarzeń domeny. Dzieje się tak, ponieważ zakres życia DbContext jest domyślnie zdefiniowany jako "zakres". W związku z tym obiekt DbContext jest współużytkowany w wielu obiektach repozytorium, które są tworzone w tym samym zakresie lub grafie obiektów. Zbiega się to z zakresem HttpRequest podczas tworzenia internetowych interfejsów API lub aplikacji MVC.

W rzeczywistości oba podejścia (pojedyncza transakcja niepodzielna i spójność ostateczna) mogą mieć rację. To naprawdę zależy od twojej domeny lub wymagań biznesowych i tego, co mówią eksperci ds. domeny. Zależy to również od tego, jak skalowalna jest usługa (bardziej szczegółowe transakcje mają mniejszy wpływ na blokady bazy danych). Zależy to od tego, ile inwestycji chcesz wykonać w kodzie, ponieważ spójność ostateczna wymaga bardziej złożonego kodu w celu wykrywania możliwych niespójności w agregacjach i konieczności zaimplementowania akcji wyrównywujących. Należy wziąć pod uwagę, że jeśli zatwierdzisz zmiany w oryginalnej agregacji, a następnie, gdy zdarzenia są wysyłane, jeśli wystąpi problem, a programy obsługi zdarzeń nie mogą zatwierdzić ich skutków ubocznych, będziesz mieć niespójności między agregacjami.

Sposobem zezwolenia na akcje wyrównywałe byłoby przechowywanie zdarzeń domeny w dodatkowych tabelach bazy danych, aby mogły być częścią oryginalnej transakcji. Następnie można mieć proces wsadowy, który wykrywa niespójności i uruchamia akcje wyrównujące, porównując listę zdarzeń z bieżącym stanem agregacji. Akcje wyrównujące są częścią złożonego tematu, który będzie wymagał szczegółowej analizy po stronie użytkownika, który obejmuje dyskusję z ekspertami w zakresie użytkowników biznesowych i domen.

W każdym razie możesz wybrać potrzebne podejście. Jednak początkowe odroczone podejście — podnoszenie zdarzeń przed zatwierdzeniem, więc używasz jednej transakcji — jest najprostszym podejściem w przypadku korzystania z platformy EF Core i relacyjnej bazy danych. Implementacja i prawidłowa w wielu przypadkach biznesowych jest łatwiejsza. Jest to również podejście używane w zamawianiu mikrousługi w eShop.

Ale jak rzeczywiście wysyłać te zdarzenia do odpowiednich programów obsługi zdarzeń? _mediator Jaki jest obiekt widoczny w poprzednim przykładzie? Ma to związek z technikami i artefaktami używanymi do mapowania między zdarzeniami a ich procedurami obsługi zdarzeń.

Dyspozytor zdarzeń domeny: mapowanie zdarzeń na programy obsługi zdarzeń

Gdy będzie można wysłać lub opublikować zdarzenia, potrzebujesz pewnego rodzaju artefaktu, który opublikuje zdarzenie, aby każdy powiązany program obsługi mógł go pobrać i przetworzyć skutki uboczne na podstawie tego zdarzenia.

Jednym z podejść jest rzeczywisty system obsługi komunikatów, a nawet magistrala zdarzeń, prawdopodobnie oparta na magistrali usług, w przeciwieństwie do zdarzeń w pamięci. Jednak w przypadku pierwszego przypadku rzeczywiste komunikaty byłyby nadmierne w przypadku przetwarzania zdarzeń domeny, ponieważ wystarczy przetworzyć te zdarzenia w ramach tego samego procesu (czyli w tej samej domenie i warstwie aplikacji).

Jak subskrybować zdarzenia domeny

W przypadku korzystania z usługi MediatR każda procedura obsługi zdarzeń musi używać typu zdarzenia podanego w parametrze ogólnym interfejsu INotificationHandler , jak widać w poniższym kodzie:

public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler
  : INotificationHandler<OrderStartedDomainEvent>

Na podstawie relacji między programem obsługi zdarzeń i zdarzeniami, które można traktować jako subskrypcję, artefakt MediatR może odnaleźć wszystkie programy obsługi zdarzeń dla każdego zdarzenia i wyzwolić każdy z tych programów obsługi zdarzeń.

Jak obsługiwać zdarzenia domeny

Na koniec program obsługi zdarzeń zwykle implementuje kod warstwy aplikacji, który używa repozytoriów infrastruktury do uzyskania wymaganych dodatkowych agregacji i wykonywania logiki domeny efekt uboczny. Poniższy kod obsługi zdarzeń domeny w eShop pokazuje przykład implementacji.

public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler
    : INotificationHandler<OrderStartedDomainEvent>
{
    private readonly ILogger _logger;
    private readonly IBuyerRepository _buyerRepository;
    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;

    public ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler(
        ILogger<ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler> logger,
        IBuyerRepository buyerRepository,
        IOrderingIntegrationEventService orderingIntegrationEventService)
    {
        _buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository));
        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async Task Handle(
        OrderStartedDomainEvent domainEvent, CancellationToken cancellationToken)
    {
        var cardTypeId = domainEvent.CardTypeId != 0 ? domainEvent.CardTypeId : 1;
        var buyer = await _buyerRepository.FindAsync(domainEvent.UserId);
        var buyerExisted = buyer is not null;

        if (!buyerExisted)
        {
            buyer = new Buyer(domainEvent.UserId, domainEvent.UserName);
        }

        buyer.VerifyOrAddPaymentMethod(
            cardTypeId,
            $"Payment Method on {DateTime.UtcNow}",
            domainEvent.CardNumber,
            domainEvent.CardSecurityNumber,
            domainEvent.CardHolderName,
            domainEvent.CardExpiration,
            domainEvent.Order.Id);

        var buyerUpdated = buyerExisted ?
            _buyerRepository.Update(buyer) :
            _buyerRepository.Add(buyer);

        await _buyerRepository.UnitOfWork
            .SaveEntitiesAsync(cancellationToken);

        var integrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(
            domainEvent.Order.Id, domainEvent.Order.OrderStatus.Name, buyer.Name);
        await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);

        OrderingApiTrace.LogOrderBuyerAndPaymentValidatedOrUpdated(
            _logger, buyerUpdated.Id, domainEvent.Order.Id);
    }
}

Poprzedni kod procedury obsługi zdarzeń domeny jest uważany za kod warstwy aplikacji, ponieważ używa repozytoriów infrastruktury, jak wyjaśniono w następnej sekcji w warstwie trwałości infrastruktury. Programy obsługi zdarzeń mogą również używać innych składników infrastruktury.

Zdarzenia domeny mogą generować zdarzenia integracji, które mają być publikowane poza granicami mikrousług

Na koniec należy wspomnieć, że czasami warto propagować zdarzenia w wielu mikrousługach. Propagacja jest zdarzeniem integracji i może zostać opublikowana za pośrednictwem magistrali zdarzeń z dowolnej procedury obsługi zdarzeń domeny.

Wnioski dotyczące zdarzeń domeny

Jak wspomniano, użyj zdarzeń domeny, aby jawnie zaimplementować skutki uboczne zmian w domenie. Aby użyć terminologii DDD, użyj zdarzeń domeny, aby jawnie zaimplementować skutki uboczne w jednej lub wielu agregacjach. Ponadto, aby zapewnić lepszą skalowalność i mniejszy wpływ na blokady bazy danych, użyj spójności ostatecznej między agregacjami w tej samej domenie.

Aplikacja referencyjna używa usługi MediatR do synchronicznego propagowania zdarzeń domeny między agregacjami w ramach jednej transakcji. Można jednak również użyć implementacji protokołu AMQP, takiej jak RabbitMQ lub Azure Service Bus , aby propagować zdarzenia domeny asynchronicznie, używając spójności ostatecznej, ale, jak wspomniano powyżej, należy rozważyć potrzebę akcji kompensacyjnych w przypadku awarii.

Dodatkowe zasoby