Auslösen eines Business Events
Am besten führst du deine Implementierung fort.
Alternativ kannst du auch folgenden Startpunkt der Implementierung im TFS wählen:
https://tfsprod.schleupen-ag.de/tfs/DefaultCollection/CS3/_git/cs-Schleupen.AS.MT.BIB?path=%2F&version=GBfeature%2FDevCampus-DomainEvents&_a=contents
Zur Implementierung verwende bitte einen Feature Branch wie in Erstellen eines Feature Branches beschrieben.
Ein Business Event ist ein Land-übergreifendes fachliches Event. Im Standard lösen die Services immer Events aus, um Funktionalität flexibel nutzen zu können. Exemplarisch implementieren wir das Business Event "Buecher ausgeliehen" als Seiteneffekt eines Domain Events.
Das Event wurde im kanonischen Modell wie folgt modelliert:

Der Payload besteht prinzipiell aus Ids, da der Subscriber sehr individuelle Anforderungen hat und die notwendigen Daten per EntityService lesen kann!
Wir prägen die Ausleihe dennoch als Typ aus, da ansonsten der nicht ausdrucksstarke Typ Guid
verwendet würde (d.h. schlussendlich in der WSDL).
1. Als erstes implementieren wir ein Gateway zum Auslösen eines Events. Hierzu legen wir im Projekt "Gateways.Contracts" im Subnamensraum Benutzerkonten.BusinessEvents.Async die zugehörige WSDL ab. Die WSDL importieren wir mit der Visual Studio Extension 'ServiceInterfaceRepository'. Diese öffnen wir wie folgt:

Dann können wir das Event suchen ...

... und in das selektierte Verzeichnis hinzufügen:

In den Eigenschaften der Wsdl hinterlegen wir als Benutzerdefiniertes Tool 'Sigento', so dass C#-Code generiert wird.
Dadurch, dass die Wsdl in einem Verzeichnis namens Async legen, wird async-Code generiert. Dieser ist perspektivisch relevant.
Das Gateway selber implementieren wir im Projekt "Gateways" im Subnamensraum Benutzerkonten.BusinessEvents.BuecherAusgeliehenEventService.V3_1:

namespace Schleupen.AS.MT.BIB.Benutzerkonten.BusinessEvents.BuecherAusgeliehenEventService.V3_1; using System; using System.Collections.Generic; using System.Linq; using Schleupen.AS.MT.BIB.Ausleihen; using Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusgeliehenEventService.V3_1; using Schleupen.CS.PI.SB.TransactionalOutbox.MessageBus.BusinessEvents; public class BuecherAusgeliehenEventServiceGateway { private readonly IBusinessEventPublisher businessEventPublisher; private readonly IAusleiheAssembler ausleiheAssembler; public BuecherAusgeliehenEventServiceGateway( IBusinessEventPublisher businessEventPublisher, IAusleiheAssembler ausleiheAssembler) { this.businessEventPublisher = businessEventPublisher ?? throw new ArgumentNullException(nameof(businessEventPublisher)); this.ausleiheAssembler = ausleiheAssembler ?? throw new ArgumentNullException(nameof(ausleiheAssembler)); } public Task RaiseAsync(IEnumerable<AusleiheId> ausleihen) { IEnumerable<AusleiheId> ausleiheIds = ausleihen as AusleiheId[] ?? ausleihen.ToArray(); if (!ausleiheIds.Any()) { return Task.CompletedTask; } businessEventPublisher.PublishOnTransactionComplete<IBuecherAusgeliehenEventService, RaisedNotification>( new RaisedNotification { AusleiheListe = ausleiheAssembler.ToDataContract(ausleiheIds) }); return Task.CompletedTask; } }
Auch hier implementieren wir einen Anticorruption Layer und erstellen dazu einen Assembler samt Schnittstelle im gleichen Verzeichnis:
namespace Schleupen.AS.MT.BIB.Benutzerkonten.BusinessEvents.BuecherAusgeliehenEventService.V3_1; using System.Collections.Generic; using Schleupen.AS.MT.BIB.Ausleihen; using Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusgeliehenEventService.V3_1; public interface IAusleiheAssembler { List<AusleiheContract> ToDataContract(IEnumerable<AusleiheId> ausleihen); }
namespace Schleupen.AS.MT.BIB.Benutzerkonten.BusinessEvents.BuecherAusgeliehenEventService.V3_1; using System.Collections.Generic; using Schleupen.AS.MT.BIB.Ausleihen; using Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusgeliehenEventService.V3_1; public sealed class AusleiheAssembler : IAusleiheAssembler { public List<AusleiheContract> ToDataContract(IEnumerable<AusleiheId> ausleihen) { if (ausleihen == null) { return new List<AusleiheContract>(); } return ausleihen.Select(ToDataContract) .Where(ausleiheContract => ausleiheContract != null) .ToList(); } private AusleiheContract ToDataContract(AusleiheId ausleihe) { if (ausleihe == null) { return null; } AusleiheContract ausleiheContract = new(); ausleiheContract.AusleiheId = ausleihe.Value; return ausleiheContract; } }
Im Projekt "Gateway.UnitTests" erstellen wir nun die folgenden Dateien

namespace Schleupen.AS.MT.BIB.Benutzerkonten.BusinessEvents.BuecherAusgeliehenEventService.V3_1; using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework; using Schleupen.AS.MT.BIB.Ausleihen; using Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusgeliehenEventService.V3_1; [TestFixture] internal sealed partial class BuecherAusgeliehenEventServiceGatewayTest { [Test] public async Task Raise_ShouldInvokeClient() { var ausleiheId = AusleiheId.New(); List<AusleiheId> ausleihen = new() { ausleiheId }; BuecherAusgeliehenEventServiceGateway testObject = fixture.CreateTestObject(); await testObject.RaiseAsync(ausleihen); fixture.Mocks.BusinessEventPublisherMock .Verify(x => x.PublishOnTransactionComplete<IBuecherAusgeliehenEventService, RaisedNotification>( It.Is<RaisedNotification>(r => Matches(r, ausleiheId)))); } private bool Matches(RaisedNotification raisedNotification, AusleiheId ausleiheId) { return raisedNotification.AusleiheListe.Any(a => a.AusleiheId == ausleiheId.Value); } }
namespace Schleupen.AS.MT.BIB.Benutzerkonten.BusinessEvents.BuecherAusgeliehenEventService.V3_1; using Moq; using NUnit.Framework; using Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusgeliehenEventService.V3_1; using Schleupen.CS.PI.SB.TransactionalOutbox.MessageBus.BusinessEvents; internal sealed partial class BuecherAusgeliehenEventServiceGatewayTest { private Fixture fixture; [SetUp] public void Setup() { fixture = new(); } private sealed class Mocks { public Mock<IBusinessEventPublisher> BusinessEventPublisherMock { get; } = new(); } private sealed class Fixture { public Mocks Mocks { get; } = new(); public BuecherAusgeliehenEventServiceGateway CreateTestObject() { Mocks.BusinessEventPublisherMock .Setup(x => x.PublishOnTransactionComplete<IBuecherAusgeliehenEventService, RaisedNotification>( It.IsAny<RaisedNotification>())); return new BuecherAusgeliehenEventServiceGateway( Mocks.BusinessEventPublisherMock.Object, new AusleiheAssembler()); } } }
Weitere Tests zur besseren Code-Abdeckung überlassen wir dem Leser!
2. Nun schließen wir das Gateway als Seiteneffekt des Domain Events "Buecher ausgeliehen" an. Hierzu erstellen wir zunächst die Schnittstelle des Gateways und erstellen die Datei IBuecherAusgeliehenEventServiceGateway.cs wie folgt:

namespace Schleupen.AS.MT.BIB.Benutzerkonten; using System.Collections.Generic; using Schleupen.AS.MT.BIB.Ausleihen; public interface IBuecherAusgeliehenEventServiceGateway { Task RaiseAsync(IEnumerable<AusleiheId> ausleihen); }
Das Gateway muss diese nun implementieren:
public class BuecherAusgeliehenEventServiceGateway : IBuecherAusgeliehenEventServiceGateway { ... }
Nun implementieren wir einen DomainEventHandler des Domain Events:

namespace Schleupen.AS.MT.BIB.Benutzerkonten.DomainEventHandler; using System; using System.Threading; using System.Threading.Tasks; using MediatR; using Schleupen.AS.MT.BIB.Benutzerkonten.DomainEvents; public sealed class RaiseBusinessEventWhenBuecherAusgeliehenDomainEventHandler : INotificationHandler<BuecherAusgeliehenDomainEvent> { private readonly IBuecherAusgeliehenEventServiceGateway buecherAusgeliehenEventServiceGateway; public RaiseBusinessEventWhenBuecherAusgeliehenDomainEventHandler(IBuecherAusgeliehenEventServiceGateway buecherAusgeliehenEventServiceGateway) { this.buecherAusgeliehenEventServiceGateway = buecherAusgeliehenEventServiceGateway ?? throw new ArgumentNullException(nameof(buecherAusgeliehenEventServiceGateway)); } public async Task Handle(BuecherAusgeliehenDomainEvent notification, CancellationToken cancellationToken) { if (notification == null) { throw new ArgumentNullException(nameof(notification)); } await buecherAusgeliehenEventServiceGateway.RaiseAsync(notification.Ausleihen); } }
3. Wir stellen das Auslösen nun durch einen ComponentTest sicher und ergänzen dazu die Klasse BuecherAusleihenActivityServiceTest
:
using Schleupen.AS.MT.BIB.Benutzerkonten.BuecherAusgeliehenEventService.V3_1; ... [TestFixture] internal sealed partial class BuecherAusleihenActivityServiceTest { ... [Test] public async Task Leihe_WhereBuchExists_ShouldRaiseBuecherAusgeliehenBusinessEvent() { Buch buch; Benutzerkonto benutzerkonto; using (ITransactionScope transactionScope = fixture.CreateTransactionScope()) { buch = fixture.CreatePersistedBuch(); benutzerkonto = fixture.CreatePersistedBenutzerkonto(); transactionScope.Complete(); } fixture.MockIdentityProvider(benutzerkonto, fixture.CreateSessionToken()); await fixture.WithOpenedServiceHostAsync( async client => { LeiheAusRequest leiheAusRequest = fixture.CreateLeiheAusRequest(buch); await client.LeiheAusAsync(leiheAusRequest); fixture.Mocks.BusinessEventPublisherMock.Verify( x => x.PublishOnTransactionComplete<IBuecherAusgeliehenEventService, RaisedNotification>( It.IsAny<RaisedNotification>())); }); } }
Wir müssen hierzu noch einen BusinessEventPublisherMock implementieren. Der Code zeigt die zu ergänzenden Code-Bestandteile.
internal sealed partial class BuecherAusleihenActivityServiceTest : IDisposable { ... private sealed class Fixture : ServiceTestFixture<IBuecherAusleihenActivityService, BuecherAusleihenActivityService> { ... public Fixture() : base(new TestServiceHostFactoryConfigurator()) { ... RegisterInstance(Mocks.BusinessEventPublisherMock.Object); } ... } ... private sealed class Mocks { ... public Mock<IBusinessEventPublisher> BusinessEventPublisherMock { get; } = new Mock<IBusinessEventPublisher>(); } }
Nun ist der Test grün:

Für Schleupen-Entwickler: Das Ergebnis der Implementierung kannst du im TFS an folgender Stelle einsehen:
https://tfsprod.schleupen-ag.de/tfs/DefaultCollection/CS3/_git/cs-Schleupen.AS.MT.BIB?path=%2F&version=GBfeature%2FDevCampus-BusinessEvents&_a=contents