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

Die Anbindung an andere Services 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:

  • Kapselung der Aufrufe eines Services (siehe Kommunikation mit WCF-Diensten)
  • Übersetzung (mittels Assembler) von Domänentypen in Schnittstellentypen (DataContracts) inklusive Request-/Response
  • Übersetzung von Fehlerverträgen zu Ausnahmen im Falle von WCF-Diensten [optional]

Design

Es wird pro anzusprechenden Service oder Namespace mit Services 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: <Servicename>Gateway

In der Gateways-Assembly werden dann die Service-Zugriffsklassen über Sigento generiert.

Es kann Sigento ab der Version 3.5.15.0 verwendet werden. Hierdurch werden folgende Schnittstellen generiert:

  • I<Servicename>Client
  • I<Servicename>ClientFactory
  • <Servicename>ClientFactory

Damit kann die Dependency Injection einfach implementiert werden und somit der ServiceClient per Testdouble ausgetauscht werden.

Folgende Trennung bzgl. der Datenverträge ist ratsam

  • Schleupen.CS.<myproduct>.Contracts - Datenverträge der bereitgestellten Services - Sigento generiert nur die Typen der WSDL, da Schleupen.CS.PI.SB.Client nicht angebunden
  • Schleupen.CS.<myproduct>.SubscriptionContracts - Datenverträge der implementierten EventServices - Sigento generiert nur die Typen der WSDL, da Schleupen.CS.PI.SB.Client nicht angebunden
  • Schleupen.CS.<myproduct>.GatewaysContracts - Datenverträge der auslösenden Events - Sigento generiert Typen der WSDL und ServiceClient, ServiceClientFactory etc.

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 Aufruf eines WCF-Dienstes gezeigt. Die Schnittstelle des Dienstes habe folgende Gestalt:

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.AusleihFristVerlaengertEventService_3.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 für einen normalen Serviceaufruf:

namespace Schleupen.CS.MT.BIB.Ausleihen.BuecherAusgeliehenEventService;

public class BuecherAusgeliehenEventServiceGateway : IBuecherAusgeliehenEventServiceGateway
{
    private readonly IBuecherAusgeliehenEventServiceClientFactory serviceClientFactory;
    private readonly IAusleiheAssembler ausleiheAssembler;

    public BuecherAusgeliehenEventServiceGateway(
        IBuecherAusgeliehenEventServiceClientFactory serviceClientFactory,
        IAusleiheAssembler ausleiheAssembler)
    {
        this.serviceClientFactory = serviceClientFactory ?? throw new ArgumentNullException(nameof(serviceClientFactory));
        this.ausleiheAssembler = ausleiheAssembler ?? throw new ArgumentNullException(nameof(ausleiheAssembler));
    }

    [SuppressMessage("Microsoft.Reliability", "CA2000:Objekte verwerfen, bevor Bereich verloren geht",
        Justification = "Durch CloseSafely wird bereits disposed.")]
    public void Raise(AusleiheListe ausleihen)
    {
        // ...
        using IBuecherAusgeliehenEventServiceClient serviceClient = serviceClientFactory.Create();
        serviceClient.Raised(new RaisedNotification() { Ausleihen = ausleiheAssembler.ToDataContract(ausleihen) });
    }
}

Die Schnittstelle IBuecherAusgeliehenEventServiceClientFactory und deren Implementierung BuecherAusgeliehenEventServiceClientFactory wird für den CS 3.0-Zugriff durch Sigento bereitgestellt! Damit diese Klassen durch Sigento generiert werden, muss Schleupen.CS.PI.SB.Client.dll angebunden sein.

Die Nutzung des Gateways sieht dabei wie folgt aus:

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);
    }
}

Die Implementierung eines Gateways für einen QueryService erfolgt analog.

Exceptions werden in dieser Implementierung auf den Aufrufer durchgeleitet und können dort gehandhabt werden. Falls spezielle Exceptions vom Aufrufer unterschieden werden sollen, sollten diese im Gateway durch einen Assembler konvertiert werden um den Aufrufer vom Service entkoppelt zu halten.

Implementierung per Async/Await

Mithilfe von Sigento beschrieben die Schnittstellen in der Gatways.Contracts-Assembly unter ein Verzeichnis Async ablegen.

Nun kann wie gewohnt mit async/await implementiert werden:

public class BuchUpdatedEventServiceGateway : IBuchUpdatedEventServiceGateway
{
    [SuppressMessage("Microsoft.Reliability", "CA2000:Objekte verwerfen, bevor Bereich verloren geht",
        Justification = "Durch CloseSafely wird bereits disposed.")]
    public async Task RaiseAsync(IEnumerable<Guid> aktualisierteBuecher)
    {
        // ...
        IBuchUpdatedEventServiceClient serviceClient = serviceClientFactory.Create();
        await serviceClient.RaisedAsync(new RaisedNotification() { Buecher = buchIdAssembler.ToDataContract(enumerable) });
    }
}
Konfiguration des Bindings (Optional)

Muss das Binding noch angepasst werden, so kann dies über eine partielle Methode erfolgen:

public partial class BuchUpdatedEventServiceClientFactory
{
    partial void OnConfigureBinding(System.ServiceModel.Channels.Binding binding)
    {
        if (binding is NetTcpBinding netTcpBinding)
        {
            netTcpBinding.MaxReceivedMessageSize = long.MaxValue;
            netTcpBinding.TransferMode = TransferMode.Buffered;
            netTcpBinding.MaxBufferSize = int.MaxValue;
            netTcpBinding.MaxBufferPoolSize = long.MaxValue;
        }

        if (binding is HttpBinding httpBinding)
        {
            httpBinding.MaxReceivedMessageSize = long.MaxValue;
            httpBinding.TransferMode = TransferMode.Buffered;
            httpBinding.MaxBufferSize = int.MaxValue;
            httpBinding.MaxBufferPoolSize = long.MaxValue;
        }
    }
}
Unittest

Code des Unittests:

[Test]
public void Raise_WithSample_ShouldCallServiceClient()
{
    BuchUpdatedEventServiceGateway testObject = fixture.CreateTestObject();

    testObject.Raise(fixture.BuchIds);

    fixture.Mocks.BuchUpdatedEventServiceClientMock.Verify(x => x.Raised(It.IsAny<RaisedNotification>()));
}

Code im Testfixture:

public BuchUpdatedEventServiceGateway CreateTestObject()
{
    ArrayOfId arrayOfId = new ArrayOfId();
    arrayOfId.AddRange(BuchIds.Select(buchId => buchId.ToString()));
    RaisedNotification raisedNotification = new RaisedNotification(SessionToken, arrayOfId);
    mocks.BuchUpdatedEventServiceClientMock.Setup(x => x.Raised(Parameter.Is(raisedNotification).Object));
    mocks.BuchUpdatedEventServiceClientFactoryMock.Setup(x => x.Create()).Returns(mocks.BuchUpdatedEventServiceClientMock.Object);

    BuchUpdatedEventServiceGateway testObject = new BuchUpdatedEventServiceGateway(mocks.BuchUpdatedEventServiceClientFactoryMock.Object);

    return testObject;
}
Hinweise (für Ausnahmefälle)

Das Muster wurde angepasst - die ursprüngliche Version hat so nicht in allen Konstellationen funktioniert. Ein ISessionTokenProvider ist nur noch in Ausnahmefällen notwendig.

[TestFixture]
public sealed class GenericServiceTest : GenericServiceTestBase
{
    protected override Assembly ServiceAssembly => typeof(BenutzerkontoEntityService).Assembly;

    protected override ContainerConfigurator ServiceConfigurator => new ServiceHostTestFactoryConfigurator();

    private sealed class ServiceHostTestFactoryConfigurator : ServiceHostFactoryConfigurator
    {
        protected override bool IsInsideWcfOperation => false;

        protected override void SetupLifestyle(Assembly assembly, BasedOnDescriptor descriptor)
        {
            if (descriptor == null) { throw new ArgumentNullException(nameof(descriptor)); }

            descriptor.LifestyleTransient();
        }

        protected override void OnContainerConfigured(IWindsorContainer windsorContainer)
        {
            windsorContainer.Register(Component.For<ISessionTokenProvider>()
               .ImplementedBy<TestSessionTokenProvider>().IsDefault());

            base.OnContainerConfigured(windsorContainer);
        }

        // ReSharper disable ClassNeverInstantiated.Local
        private sealed class TestSessionTokenProvider : ISessionTokenProvider
        // ReSharper restore ClassNeverInstantiated.Local
        {
            // ReSharper disable once UnusedAutoPropertyAccessor.Local
            public string Token { get; private set; }

            public bool TryGetToken(out string token)
            {
                token = Token;
                return true;
            }
        }
    }
}
Zugriff auf CS 2.0

Soll der Zugriff auf CS 2.0 über Services erfolgen, so muss folgendes angepasst werden:

  • Abrufen der WSDL aus einem CS 2.0-System
  • Einbinden der WSDL und Anschluss von Sigento als benutzerdefiniertes Tool Sigento.Pure
  • Implementierung der Factory mit Anschluss an die Konfigurationsverwaltung und der ILegacySessionXmlFactory
Beispiel

Das Gateway wird wie gewohnt implementiert:

public class UtilmdMeldungSuchenActivityServiceGateway : IUtilmdMeldungSuchenActivityServiceGateway
{
    private readonly string sessionXml;
    private readonly IUtilmdMeldungSuchenActivityServiceClientFactory serviceFactory;
    private readonly ISuchergebnisAssembler suchergebnisAssembler;

    public UtilmdMeldungSuchenActivityServiceGateway(
        ILegacySessionXmlFactory legacySessionXmlFactory,
        IUtilmdMeldungSuchenActivityServiceClientFactory serviceFactory,
        ISuchergebnisAssembler suchergebnisAssembler)
    {
        sessionXml = legacySessionXmlFactory.CreateSessionXml();
        this.serviceFactory = serviceFactory ?? throw new ArgumentNullException(nameof(serviceFactory));
        this.suchergebnisAssembler = suchergebnisAssembler ?? throw new ArgumentNullException(nameof(suchergebnisAssembler));
    }

    public IEnumerable<UtilmdSuchergebnis> QueryByIdentifikaiton(IEnumerable<UtilmdSuchparameter> suchparameter)
    {
        ExecuteRequest request = new ExecuteRequest();
        // ...
        using IUtilmdMeldungSuchenActivityServiceClient client = serviceFactory.Create();
        ExecuteResponse response = client.Execute(request);
        return suchergebnisAssembler.ToDomainObject(response.SuchergebnisListe);
    }
}

Mithilfe der ILegacySessionXmlFactory erhält man das passende SessionXml für CS 2.0. Die WSL entweder per Import WSDL erstellen oder manuell im EA erstellen.

Eine eigene Implementierung für die ClientFactory erstellen:

public class ConfigurableUtilmdMeldungSuchenActivityServiceClientFactory : IUtilmdMeldungSuchenActivityServiceClientFactory
{
    private readonly IBrokerClientConfiguration clientConfiguration;
    private readonly IResolveEndpointActivityServiceGateway resolveEndpointActivityServiceGateway;

    public UtilmdMeldungSuchenActivityServiceClientFactory(
        IBrokerClientConfiguration clientConfiguration,
        IResolveEndpointActivityServiceGateway resolveEndpointActivityServiceGateway)
    {
        this.clientConfiguration = clientConfiguration ?? throw new ArgumentNullException(nameof(clientConfiguration));
        this.resolveEndpointActivityServiceGateway = resolveEndpointActivityServiceGateway ?? throw new ArgumentNullException(nameof(clientConfiguration));
    }

    public IUtilmdMeldungSuchenActivityServiceClient Create()
    {
        string applikationsserverAdresse = resolveEndpointActivityServiceGateway.ErmittleApplikationsserverAdresse();
        string uri = $"{applikationsserverAdresse}CS.NM/Zaehlerstandsuebermittlung/IOP/UtilmdMeldungSuchenActivityService.svc";
        return new UtilmdMeldungSuchenActivityServiceClient(clientConfiguration.GetBinding(Uri.UriSchemeHttp), new EndpointAddress(uri));
    }
}

Hierbei wird die Endpunktverwaltung verwendet:

public sealed class ResolveEndpointActivityServiceGateway : IResolveEndpointActivityServiceGateway
{
    private const string DefaultEndpoint = "http://localhost/Schleupen/";

    private readonly IResolveEndpointActivityServiceClientFactory serviceClientFactory;
    private readonly SystemUsage systemUsage;

    public ResolveEndpointActivityServiceGateway(
        IResolveEndpointActivityServiceClientFactory serviceClientFactory,
        ISystemUsageProvider systemUsageProvider)
    {
        this.serviceClientFactory = serviceClientFactory ?? throw new ArgumentNullException(nameof(serviceClientFactory));
        this.systemUsage = systemUsageProvider.GetSystemUsageOf(sessionToken) ?? throw new ArgumentNullException(nameof(systemUsageProvider));
    }

    public string ErmittleApplikationsserverAdresse()
    {
        ErzeugeExecuteRequest request = ErzeugeExecuteRequest();

        using (IResolveEndpointActivityServiceClient serviceClient = serviceClientFactory.Create())
        {
            ErzeugeExecuteRespnse response = serviceClient.Execute(request);
            return ErmittleZuVerwendendenEndpunktZurErgebnisliste(response?.EndpointListe, systemUsage.Id);
        }
    }

    private ExecuteRequest ErzeugeExecuteRequest()
    {
        return new ExecuteRequest
        {
            EndpointKeys = new List<ArtifactIdentifierContract>
            {
                new ArtifactIdentifierContract
                {
                    ExternalVersion = "3.0",
                    Name = "Applikationsserver",
                    Namespace = "Schleupen.CS.2_0"
                }
            }
        };
    }

    private string ErmittleZuVerwendendenEndpunktZurErgebnisliste(IEnumerable<EndpointContract> endpoints, Guid systemUsageId)
    {
        var endpointArray = endpoints?.ToArray() ?? new EndpointContract[0];
        if (!endpointArray.Any())
        {
            return DefaultEndpoint;
        }

        var endpointContract = endpointArray.First();

        foreach (var endpointAddressContract in endpointContract.EndpointAddressListe)
        {
            if (endpointAddressContract.SystemUsageId.HasValue)
            {
                if (endpointAddressContract.SystemUsageId.Value == systemUsageId)
                {
                    return endpointAddressContract.Address;
                }
            }
            else
            {
                return endpointAddressContract.Address;
            }
        }

        return DefaultEndpoint;
    }
}

Per Castle eine eigene Implementierung von IUtilmdMeldungSuchenActivityServiceClientFactory registrieren (nicht die Sigento-Version!).

Abgrenzung

OneWay-Services werden mit diesem Muster nicht unterstützt.

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 (aka Separation of Concerns)
  • Die Lesbarkeit des Codes wird verbessert.
  • Vermeidung von redundantem Code durch Wiederverwendung
  • Unproblematisch hinsichtlich Versionierung
  • Einfache Migration bzgl. der Verwendung der Transactional Outbox
Schwächen
  • Initialer Implementierungsaufwand (minimiert durch Codegenerierung mittels Sigento).
  • Bei Events: Magie und wenn Transactional Outbox nicht angebunden ist merkt evtl. nicht, dass diese nicht angeschlossen ist => in diesem Fall besser das Muster Gateway zu entfernten Objekten - Events verwenden!
Cookie Consent mit Real Cookie Banner