Work in Progress
Diese Seite ist aktuell im Review! Die Seite wurde noch nicht qualitätsgesichert und kann Fehler enthalten.
Die verlinkten Seiten sind ggf. nur für Schleupen-Mitarbeiter sichtbar.

Gateway zu entfernten Objekten - Commands

Dieses Dokument definiert das Muster Gateway für das Auslösen eines Commands über einen Message Broker (RabbitMQ). Die hierfür erforderliche Schnittstelle wird wie in Command 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: BuchVerlustMeldenCommandServiceGateway

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: IBuchVerlustMeldenCommandServicGateway

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.BuchVerlustMeldenCommandService.V3_1;

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(Namespace = "urn://Schleupen.CS.MT.BIB.BuchVerlustMeldenCommandService_3.1",
    ConfigurationName = "Schleupen.CS.MT.BIB.BuchVerlustMeldenCommandService.V3_1.BuecherAusgeliehenEventService")]
public interface IBuecherAusgeliehenEventService
{
    [System.ServiceModel.OperationContractAttribute(
        Action = "urn://Schleupen.CS.MT.BIB.IBuchVerlustMeldenCommandService_3.1/Raised",
        ReplyAction = "urn://Schleupen.CS.MT.BIB.IBuchVerlustMeldenCommandService_3.1/RaisedResponse")]
    Schleupen.CS.MT.BIB.BuchVerlustMeldenCommandService.V3_1.ExecuteResponse Execute(
        Schleupen.CS.MT.BIB.BuchVerlustMeldenCommandService.V3_1.Execute request);
}

Die Schnittstelle des Gateways sieht dann wie folgt aus:

namespace Schleupen.CS.MT.BIB.Ausleihen;

public interface IBuchVerlustMeldenCommandService
{
    void Melden(BuchListe buecher);
}

Die eigentliche Implementierung des internen Gateways zeigt der folgende Code unter Verwendung der Transactional Outbox:

namespace Schleupen.CS.MT.BIB.Ausleihen.BuchVerlustMeldenCommandService.V3_1;

public class BuchVerlustMeldenCommandService : IBuchVerlustMeldenCommandServiceGateway
{
    private readonly ICommandMessageSender commandMessageSender;
    private readonly ISessionTokenProvider sessionTokenProvider;
    private readonly IBuchAssembler buchAssembler;

    public BuecherAusgeliehenEventServiceGateway(
        ICommandMessageSender commandMessageSender,
                    ISessionTokenProvider sessionTokenProvider,
        IBuchAssembler buchAssembler)
    {
        this.commandMessageSender = commandMessageSender ?? throw new ArgumentNullException(nameof(commandMessageSender));
        this.sessionTokenProvider = sessionTokenProvider ?? throw new ArgumentNullException(nameof(sessionTokenProvider));
        this.buchAssembler = buchAssembler ?? throw new ArgumentNullException(nameof(buchAssembler));
    }

    public void Melden(IBuchListe buecher)
    {
        // ...
        ExecuteRequest request = new ExecuteRequest(sessionTokenProvider.SessionToken, buchAssembler.ToDataContract(buecher));
        commandMessageSender.DeliverOnTransactionComplete<IBuchVerlustMeldenCommandService, ExecuteRequest>(request);
    }
}

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 KundenInfoEntgegennehmenController : IKundenInfoEntgegennehmenController
{
    private readonly IBuchVerlustMeldenCommandServiceGateway buchVerlustMeldenCommandServiceGateway;
    // ...

    public BuecherAusleihenController(...
			IBuchVerlustMeldenCommandServiceGateway buchVerlustMeldenCommandServiceGateway, ...)
    {
        // ...
        this.buchVerlustMeldenCommandServiceGateway = buchVerlustMeldenCommandServiceGateway
            ?? throw new ArgumentNullException(nameof(buchVerlustMeldenCommandServiceGateway));
    }

    // ...
    public void VerlustMelden(IBuchListe buecher)
    {
        // ...
        buchVerlustMeldenCommandServiceGateway.Melden(buecher);
    }
}
Unittest

Code des Unittests:

namespace Schleupen.CS.MT.BIB.Ausleihen.BuchVerlustMeldenCommandService.V3_1;

internal sealed partial class BuchVerlustMeldenCommandServiceTest
{
    // ...
    [Test]
    public void Raise_WithSample_ShouldCallServiceClient()
    {
        BuchVerlustMeldenCommandServiceGateway testObject = fixture.CreateTestObject();

        testObject.Raise(fixture.BuchIds);

        fixture.Mocks.CommandSenderMock.Verify(x =>
            x.DeliverOnTransactionComplete<IBuchVerlustMeldenCommandServiceGateway, ExecuteRequest>(
                It.IsAny<ExecuteRequest>()));
    }
}

Code im Testfixture:

namespace Schleupen.CS.MT.BIB.Ausleihen.BuchVerlustMeldenCommandService.V3_1;

internal sealed partial class BuchVerlustMeldenCommandServiceTest
{
    private sealed class Fixture
    {
        // ...

        public BuchVerlustMeldenCommandServiceGateway CreateTestObject()
		{
            // ...
			ExecuteRequest executeRequest = new ExecuteRequest(SessionToken, ...);
			mocks.BusinessEventPublisherMock.Setup(x => 
                x.DeliverOnTransactionComplete<IBuchVerlustMeldenCommandServiceGateway, ExecuteRequest>(
                    Parameter.Is(executeRequest).Object));

			BuchVerlustMeldenCommandServiceGateway testObject =
				new BuchVerlustMeldenCommandServiceGateway(
					mocks.CommandSenderMock.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)

Benutze Muster

Alternative Muster

Cookie Consent mit Real Cookie Banner