Domain Events
Um Seiteneffekte von Aggregat-Änderungen zu implementieren, werden an vielen Stellen Domain Events verwendet. Dabei ist ein Domain Event ein fachliches Ereignis, das in der Vergangenheit eingetreten ist. Daher werden diese auch in der Vergangenheitsform formuliert.
Beispiel eines Domain Events: Buecher ausgeliehen.
Domain Events drücken Seiteneffekte explizit aus und sorgen zudem für eine notwendige Code-Entkopplung bei der Implementierung. Ein möglicher Seiteneffekt ist das Auslösen eines Business Events, um Änderungen über Bausteingrenzen hinweg zu propagieren. Ein anderes Beispiel für einen Seiteneffekt ist, das Herstellen Aggregate-übergreifender Konsistenz.
Schleupen.CS trennt hier also explizit zwischen Events in der Domäne, die ihren Ursprung im Aggregat haben (→ Domain Events) und denen, die über Microservices-Grenzen hinweg fungieren (→ Business Events).
Die Namen der Events sollten auch hier der Ubiquitous Language (allgegenwärtige Sprache) entnommen werden.
Da diese Events konform zum DDD-Ansatz Fachlichkeit ausdrücken, bietet sich die Modellierungstechnik Event-Storming an.
Im Beispiel der vorstehenden Abbildung sieht man, dass das Domain Event BuecherAusgeliehen durch das Aggregat Ausleihe in der Domäne ausgelöst wird. Hierauf wird als Seiteneffekt an dem Aggregat Buch vermerkt, dass dieses ausgeliehen ist.
Ein Domain Event hat im Normalfall die Id des auslösenden Aggregats.
In der Implementierung werden sogenannte Event-Handler implementiert um diese Events zu abonnieren. Diese sind ähnlich zu Usecase Controllern und binden genauso Repositorys, Domäne und Gateways zusammen. Diese sorgt für eine sehr gute Entkopplung und ist somit sehr gut testbar.
Im Beispiel heißt dieser Handler MarkiereBuecherWhenBuecherAusgeliehenDomainEventHandler
und wird ebenfalls den Business Cases zugeordnet.
In der Implementierung erfolgt das Auslösen eines Domain Events und das Propagieren zeitlich versetzt (vgl. a better domain events pattern). Dies ist wichtig, da Events im Einklang mit lokalen Transaktionen ausgeführt werden müssen. Ausgelöst wird das Domain Event im Usecase Controller im Standard durch das Flush()
im Repository.
public class BuecherAusleihenController: IBuecherAusleihenController { private readonly IAusleiheRepository ausleiheRepository; private readonly IReadOnlyBuchRepository buchRepository; ... public async Task<IAusleihen> LeiheAusAsync(Sid benutzerSid, IEnumerable<BuchId> buecherIds) { ... Benutzerkonto benutzerkonto = await QueryBenutzerkontoAsync(benutzerSid); IBuecher auszuleihendeBuecher = ... IAusleihen ausleihen = benutzerkonto.LeiheAus(auszuleihendeBuecher); ausleiheRepository.Add(ausleihen); // Hinterlegt Domain Events aus ... await ausleiheRepository.FlushAsync(); // Löst Domain Events aus return ausleihen; } }
Wichtig hierbei ist, dass jeder Usecase Controller und jeder Domain Event-Handler genau ein Aggregate editiert - im vorherigen Code-Beispiel die Ausleihe. Um hier die Lesbarkeit zu erhöhen, implementieren die Repositorys eine Read-Only-Schnittstelle, hier IReadOnlyBuchRepository
. Die folgende Abbildung zeigt schematisch das Vorgehen, bei dem im ersten Schritt das Aggregate Ausleihe editiert wird, dann ein Domain Event ausgelöst wird und in Schritt 3 das Buch-Aggregate editiert wird.
In der Domänenklasse Ausleihe
wird zum späteren Auslösen das Event hinterlegt (hier über die Methode AddDomainEvent(...)
). Über das Interface IHaveDomainEvents<TDomainEvent>
wird markiert, dass das Aggregat ein bestimmtes Event auslöst, womit man dann eine gute Code-Navigation erreicht. Die Events, die ausgelöst werden, werden als Typparameter von IHaveDomainEvents<TDomainEvent>
angegeben. Werden mehrere unterschiedliche Events ausgelöst, so muss die Schnittstelle mehrfach angegeben werden. Über domainEventContainer.ValidateDomainEventsFor(...)
kann sichergestellt werden, dass kein Event ausgelöst wird, welches nicht deklariert ist.
public partial class Ausleihe : ... IHaveDomainEvents<BuecherAusgeliehenDomainEvent>, IHaveDomainEvents<BuecherZurueckgegebenDomainEvent> { public static Ausleihe VermerkeAusleihe(BenutzerkontoId benutzerkonto, DateTimeOffset ausgeliehenAm, BuchId buch) { Ausleihe ausleihe = new(AusleiheId.New(), benutzerkonto, ausgeliehenAm, buch); ausleihe.domainEventContainer.AddDomainEvent(new BuecherAusgeliehenDomainEvent(new[] { ausleihe.Id })); return ausleihe; } private readonly DomainEventContainer domainEventContainer = new DomainEventContainer(); ... public Ausleihe(AusleiheId id, BenutzerkontoId benutzerkontoId, DateTimeOffset ausgeliehenAm, BuchId buchId) : base(id) { ... domainEventContainer.ValidateDomainEventsFor(this); } ... IEnumerable<INotification> IHaveDomainEvents.DomainEvents => domainEventContainer.DomainEvents; bool IHaveDomainEvents.HasDomainEvents => domainEventContainer.HasDomainEvents; ... }
Das Event selber wird durch Implementierung des Marker-Interfaces INotification
verarbeitet.
public class BuecherAusgeliehenDomainEvent : INotification { public BuecherAusgeliehenDomainEvent(IAusleihen ausleihen) { Ausleihen = ausleihen ?? throw new ArgumentNullException(nameof(ausleihen)); } public IAusleihen Ausleihen { get; } ... }
Ein exemplarischer Handler sieht wie folgt aus:
public class MarkiereBuecherWhenBuecherAusgeliehenDomainEventHandler : INotificationHandler<BuecherAusgeliehenDomainEvent> { private readonly IBuchRepository buchRepository; ... public async Task Handle(BuecherAusgeliehenDomainEvent notification, CancellationToken cancellationToken) { ... IBuecher buecher = await buchRepository.QueryByIdAsync(notification.Ausleihen.SelectBuchIds()).ConfigureAwait(false); buecher?.VermerkeAlsAusgeliehen(); await buchRepository.FlushAsync().ConfigureAwait(false); } ... }
Änderungen mehrerer Aggregate innerhalb von Activity Services können und dürfen in einer Transaktion durchgeführt werden. Dabei wird wie oben beschrieben ein Aggregate geändert, ein Domain Event ausgelöst und das nächste Aggregate geändert, ein Domain Event ausgelöst usw. Die Möglichkeit diese in einer Transaktion zu klammern ist unterschiedlich zu dem Vorgehen von Vaughn Vernon, dessen Vorgehen es ist, jedes Aggregat in einer eigenen Transaktion zu ändern und dies zeitlich zu entkoppeln. Dies ist jedoch deutlich aufwendiger zu implementieren, da mehr Asynchronität implementiert wird. Bei geeignetem Schnitt ist es daher akzeptabel mehrere Aggregate in einer Transaktion zu implementieren, wenn der Schnitt ausreichend ist und die Laufzeit nicht zu groß wird. Siehe hierzu https://docs.microsoft.com/de-de/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation.