Gateway zu entfernten Objekten - Events
Dieses Dokument definiert das Muster Gateway für das publizieren von Events über einen Message Broker (RabbitMQ). Die hierfür erforderliche Schnittstelle wird wie Event Service erzeugt.
Design
Das Versenden von Nachrichten an Services (MessageConsumer) kann über ein sogenanntes Gateway realisiert werden: Ein Objekt, das den Zugriff auf ein externes System oder eine externe Ressource kapselt. (http://martinfowler.com/eaaCatalog/gateway.html). Es stellt also ein Tor zu externen Systemen oder Ressourcen dar. Dabei hat das Gateway bei uns folgende Verantwortlichkeit:
- Übersetzung (mittels Assembler) von Domänentypen in Schnittstellentypen (DataContracts) inklusive Request
- Kapselung des Aufrufs zum Message Broker (RabbitMQ) durch Nutzung der Transactional Outbox
Die versendenden Nachrichtentypen werden mithilfe von Sigento in einem geeigneten fachlichen Namensraum in der Gateways.Contracts-Assembly generiert.
Es wird pro anzusprechenden Service (MessageConsumer) ein Gateway erstellt. Die Gateways werden in den jeweiligen zugehörigen fachlichen Namensräumen in der Gateways-Assembly abgelegt (z.B. PersonChangedEventServiceGateway
im Namensraum Personen).
Für jeden benötigten Schnittstellenaufruf wird im Gateway eine entsprechende Methode implementiert. Die Schnittstellentypen des Service sollten in der Methode des Gateways nicht nach außen gegeben werden: Ein Gateway hat in seinem Methoden keine Datenverträge, sondern im Idealfall nur Domänenobjekte. Das heißt, dass diese im Gateway mit Hilfe eines Assemblers in die zugehörigen Schnittstellentypen übersetzt werden.
Namenskonvention: <ServiceInterfaceName>Gateway
Beispiel: BuecherAusgeliehenEventServiceGateway
Ferner wird pro Gateway eine Schnittstelle für den Zugriff definiert und in der BusinessCases-Assembly angelegt.
Damit kann die Dependency Injection einfach verwendet werden lautet die Namenskonvention:
Namenskonvention: I<ServiceInterfaceName>Gateway
Beispiel: IBuchVerlustMeldenCommandServiceGateway
Implementierung
Das folgende Beispiel zeigt die Anbindung eines Serviceaufrufs im WCF-Aufrufkontext.
Anmeldung im IoC-Container
public sealed class MyContainerConfigurator : WcfContainerConfigurator { protected override IEnumerable<Assembly> Assemblies { get { // ... yield return typeof(SomeServicesGateway).Assembly; } } }
Implementierung eines Gateways
Im Folgenden wird der applikationsinterne Aufrufs gezeigt. Die C#-Schnittstelle habe folgende Gestalt, wurde in der Gateways.Contracts-Assembly mithilfe von Sigento auf Basis einer WSDL generiert:
namespace Schleupen.CS.MT.BIB.BuecherAusgeliehenEventService.V3_1; [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] [System.ServiceModel.ServiceContractAttribute(Namespace = "urn://Schleupen.CS.MT.BIB.BuecherAusgeliehenEventService.1", ConfigurationName = "Schleupen.CS.MT.BIB.BuecherAusgeliehenEventService.V3_1.BuecherAusgeliehenEventService")] public interface IBuecherAusgeliehenEventService { [System.ServiceModel.OperationContractAttribute( Action = "urn://Schleupen.CS.MT.BIB.IBuecherAusgeliehenEventService_3.1/Raised", ReplyAction = "urn://Schleupen.CS.MT.BIB.IBuecherAusgeliehenEventService_3.1/RaisedResponse")] Schleupen.CS.MT.BIB.BuecherAusgeliehenEventService.V3_1.RaisedResponse Raised( Schleupen.CS.MT.BIB.BuecherAusgeliehenEventService.V3_1.RaisedNotification request); }
Die Schnittstelle des Gateways sieht dann wie folgt aus:
namespace Schleupen.CS.MT.BIB.Ausleihen; public interface IBuecherAusgeliehenEventServiceGateway { void RaiseSuccess(AusleiheListe ausleihen); }
Die eigentliche Implementierung des internen Gateways zeigt der folgende Code unter Verwendung der Transactional Outbox:
namespace Schleupen.CS.MT.BIB.Ausleihen.BuecherAusgeliehenEventService; public class BuecherAusgeliehenEventServiceGateway : IBuecherAusgeliehenEventServiceGateway { private readonly IBusinessEventPublisher businessEventPublisher; private readonly ISessionTokenProvider sessionTokenProvider; private readonly IAusleiheAssembler ausleiheAssembler; public BuecherAusgeliehenEventServiceGateway( IBusinessEventPublisher businessEventPublisher, ISessionTokenProvider sessionTokenProvider, IAusleiheAssembler ausleiheAssembler) { this.businessEventPublisher = businessEventPublisher ?? throw new ArgumentNullException(nameof(businessEventPublisher)); this.sessionTokenProvider = sessionTokenProvider ?? throw new ArgumentNullException(nameof(sessionTokenProvider)); this.ausleiheAssembler = ausleiheAssembler ?? throw new ArgumentNullException(nameof(ausleiheAssembler)); } public void Raise(IAusleiheListe ausleihen) { // ... businessEventPublisher.PublishOnTransactionComplete<IBuecherAusgeliehenEventService, RaisedNotification>( new RaisedNotification { SessionToken = sessionTokenProvider.Token, Ausleihen = ausleiheAssembler.ToDataContract(ausleihen) }); } }
Ab Schleupen.CS.PI.SB.API, Version 3.29.1.17 muss das SessionToken nicht mehr gesetzt werden. Diese wird dabei dann nur gesetzt, wenn kein SessionToken (wie im Beispiel) gesetzt wurde, die Eigenschaft oder das Feld schreibend und lesend verwendet werden kann!Beachte, dass das SessionToken nicht mehr gesetzt werden muss
Die Nutzung des Gateways sieht dabei wie folgt aus:
namespace Schleupen.CS.MT.BIB.Ausleihen; internal sealed class BuecherAusleihenController : IBuecherAusleihenController { private readonly IBuecherAusgeliehenEventServiceGateway buecherAusgeliehenEventServiceGateway; // ... public BuecherAusleihenController( ... IBuecherAusgeliehenEventServiceGateway buecherAusgeliehenEventServiceGateway, ...) { // ... this.buecherAusgeliehenEventServiceGateway = buecherAusgeliehenEventServiceGateway ?? throw new ArgumentNullException(nameof(buecherAusgeliehenEventServiceGateway)); } // ... public void RaisedAusgeliehenEvent(IAusleiheListe ausleihen) { // ... buecherAusgeliehenEventServiceGateway.Raise(ausleihen); } }
Unittest
Code des Unittests:
namespace Schleupen.CS.MT.BIB.Ausleihen.BuecherAusgeliehenEventService; internal sealed partial class BuecherAusgeliehenEventServiceTest { // ... [Test] public void Raise_WithSample_ShouldCallServiceClient() { BuchUpdatedEventServiceGateway testObject = fixture.CreateTestObject(); testObject.Raise(fixture.BuchIds); fixture.Mocks.BusinessEventPublisherMock.Verify(x => x.PublishOnTransactionComplete<IBuecherAusgeliehenEventService, RaisedNotification>( It.IsAny<RaisedNotification>())); } }
Code im Testfixture:
namespace Schleupen.CS.MT.BIB.Ausleihen.BuecherAusgeliehenEventService; internal sealed partial class BuecherAusgeliehenEventServiceTest { private sealed class Fixture { // ... public BuchUpdatedEventServiceGateway CreateTestObject() { ArrayOfId arrayOfId = new ArrayOfId(); arrayOfId.AddRange(BuchIds.Select(buchId => buchId.ToString())); RaisedNotification raisedNotification = new RaisedNotification(SessionToken, arrayOfId); mocks.BusinessEventPublisherMock.Setup(x => x.PublishOnTransactionComplete<IBuecherAusgeliehenEventService, RaisedNotification>( Parameter.Is(raisedNotification).Object)); BuchUpdatedEventServiceGateway testObject = new BuchUpdatedEventServiceGateway( mocks.BusinessEventPublisherMock.Object); return testObject; } } }
Stärken und Schwächen
Stärken
- Die Testbarkeit wird erhöht, da das Gateway einfach hinter einer Schnittstelle versteckt werden kann und somit diese durch einen Testdouble (Mock, Stub, Spy, etc.) ersetzt werden kann
- Trennung von Verantwortlichkeit (Separation of Concerns)
- Die Lesbarkeit des Codes wird verbessert
- Vermeidung von redundantem Code durch Wiederverwendung
- Unproblematisch hinsichtlich Versionierung
Schwächen
- Initialer Implementierungsaufwand (minimiert durch Codegenerierung mittels Sigento)