Gateways

Die Anbindung von Services anderer Länder oder das Auslösen von Business Events kann über ein sogenanntes Gateway realisiert werden. Ein Gateway ist ein Objekt, das den Zugriff auf ein externes System oder eine externe Ressource kapselt. Dabei hat ein Gateway in Schleupen.CS 3.0 folgende Verantwortlichkeit:

  • Kapselung der Aufrufe eines Services / des Publishen von Messages
  • Übersetzung von Domänenobjekten in Schnittstellentypen (DataContracts) mittels Assembler
  • Übersetzung von Fehlerverträgen zu Ausnahmen im Falle von WCF-Diensten [optional]
Verortung von Gateways in der Mikroarchitektur

Genauer dürfen Gateways gemäß der Servicenutzungsregeln der Makroarchitektur

  • lesende synchrone Services aufrufen,
  • Business Events auslösen,
  • oder Command Services aufrufen, die asynchron und schreibend sind.

Für jeden dieser Fälle wir unter Infrastructure im Onion Model ein eigenes Gateway für den Zugriff auf den jeweiligen Zielservice implementiert. Die Schnittstelle für den Zugriff wird für jede der drei Möglichkeiten strukturell identisch implementiert. Die Schnittstelle eines Gateways wird in dem Application Core definiert, um die Dependency-Inversion zu realisieren (vgl. Onion-Modell):

Nachfolgende Abbildung zeigt die zugehörige Projektstruktur:

Bausteine der Schale 'Infrastructure' und dessen Zuordnung in einer Solution
Aufruf eines lesenden synchronen Service

Der Aufruf eines lesenden Service per Gateway - typischerweise eines Query Service oder einer Query-Operation eines Entity Service - kann einfach angebunden werden.

Hinweis: Generell ist es ein Anti-Pattern, Ketten von Service-Aufrufen zu implementieren, da hier die Wahrscheinlichkeit von Timeouts steigt und dies die Autonomie der Bausteine konterkariert (siehe auch Resilienz). Daher ist beim Anschluss eines lesenden synchronen Service Vorsicht geboten: Bei der Nutzung einer Query-Operation eines Entity Serivces ist dies gemäß der Service-Kategorien eher unproblematisch, da Entity Serivces keine weiteren Services aufrufen dürfen. Insbesondere die Abfrage per Id ist tendenziell unkritisch. Bei Query Services hängt dies von der Implementierung ab, kann also insbesondere dann problematisch sein, wenn diese durch die orchestrierenden GP-Komponenten bereitgestellt werden!

Die Implementierung eines Gateways zum BuchEntityService sieht beispielsweise wie folgt aus:

public class BuchEntityServiceGateway : IBuchEntityServiceGateway
{
  private readonly IBuchEntityServiceClientFactory serviceClientFactory;
  private readonly IBuchAssembler buchAssembler;

  public BuchEntityServiceGateway(IBuchEntityServiceClientFactory serviceClientFactory, IBuchAssembler buchAssembler)
  {
    this.serviceClientFactory = serviceClientFactory ?? throw new ArgumentNullException(nameof(serviceClientFactory));
    this.buchAssembler = buchAssembler ?? throw  new ArgumentNullException(nameof(buchAssembler));
  }

  public async Task<IEnumerable<Buch>> QueryByIdAsync(IEnumerable<BuchId> buchIds)
  {
    if (buchIds == null) { throw new ArgumentNullException(nameof(buchIds)); }

    using (IBuchEntityServiceClient serviceClient = serviceClientFactory.Create())
    {
      QueryByIdResponse requestResult =
        await serviceClient.QueryByIdAsync(
          new QueryByIdRequest(buchAssembler.ToDataContract(buchIds)));
      return buchAssembler.ToDomainObject(requestResult.BuchListe);
    }
  }
}

Dabei werden der IBuchEntityServiceClient und die IBuchEntityServiceClientFactory mithilfe eines internen Tools (Sigento - ServiceInterfaceGenerationTool) auf Basis einer WSDL erzeugt.

Da pro Service ein Gateway erstellt wird, gilt folgende Namenskonvention:

<Service-Name>Gateway
wobei <Service-Name> der Name des Service ist.

Wie am Code ersichtlich ist, wird mithilfe einer Factory ein Service-Client erzeugt, über den der Ziel-Service aufgerufen wird. Zur Übersetzung der Domänenobjekte in DataContracts der Schnittstelle und umgekehrt wird ein Assembler implementiert, der den Anticorruption Layer umsetzt. Im Allgemeinen wird ein Assembler pro Aggregat erstellt. Dabei gilt folgende Namenskonvention: 

<AggregateRoot-Name>Assembler
wobei <AggrgateRoot-Name> der Name des Aggregate Roots ist.

Aufruf eines asynchronen schreibenden Service

Ein asynchron schreibender Service verwendet auf der Verarbeitungsstrecke eine Queue zur Pufferung. Damit gemäß der Resilienzprinzipien von Schleupen.CS die Übergabe im Zusammenspiel auch mit Transaktionen sicher erfolgt, wird eine Transactional Outbox im Gateway angeschlossen. Das Zusammenspiel der Bausteine ist in folgendem Diagramm dargestellt und deutet den Ablauf an. Der Windows-Dienst Schleupen Business Event Dispatcher entnimmt dabei die Nachricht aus der Queue und ruft den Ziel-Service (= Subscriber-Service) auf.

Aufruf eines asynchronen schreibenden Service

Die Implementierung des Aufrufs zum Queuen einer Command-Message mithilfe einer Schleupen.CS-Library ist recht einfach:

public sealed class DoSomethingActivityServiceGateway : IDoSomethingActivityServiceGateway
{
 private readonly ICommandMessageSender commandMessageSender;
 ...
 public void Execute(IEnumerable<Guid> newSomethings)
 {
  ExecuteRequest request = new(sessionToken, newSomethings);
  commandMessageSender.DeliverOnTransactionComplete<IDoSomethingActivityService, ExecuteRequest>(request); // -> transaktional, speichert innerhalb der Transaktion in der Tabelle 'OutboxMessage' // sendet die Message bei Transaktionsabschluss direkt an die Queue und löscht dieses aus der Tabelle
 }
}

Wie im synchronen Fall ist auch hier i.A. ein Assembler angeschlossen, der wie oben beschrieben implementiert wird.

Ohne Resilienzmechanismus, den man durch Verwendung der Transactional Outbox bekommt, kann auch über einen spezifischen Endpunkt das Business Event ausgelöst werden. Dies ist allerdings im Zusammenspiel mit Transaktionen nicht zu empfehlen.

Auslösen eines Business Events

Das Auslösen eines Business Events erfolgt ebenfalls über ein Gateway. Auch hier ist ein Assembler zur Übersetzung der Domänenobjekte in Datenverträge (POCOs) angeschlossen.

Klassen eines Event Service-Gateways

Das bildet sich wie folgt auf C#-Code ab:

public class BuecherAusgeliehenEventServiceGateway : IBuecherAusgeliehenEventServiceGateway
{
  private readonly ISessionTokenProvider sessionTokenProvider;
  private readonly IBusinessEventPublisher businessEventPublisher;
  private readonly IAusleiheAssembler ausleiheAssembler;

  public BuecherAusgeliehenEventServiceGateway(
    ISessionTokenProvider sessionTokenProvider,
    IBusinessEventPublisher businessEventPublisher,
    IAusleiheAssembler ausleiheAssembler)
  {
    this.sessionTokenProvider = sessionTokenProvider ?? throw new ArgumentNullException(nameof(sessionTokenProvider));
    this.businessEventPublisher = businessEventPublisher ?? throw new ArgumentNullException(nameof(businessEventPublisher));
    this.ausleiheAssembler = ausleiheAssembler ?? throw new ArgumentNullException(nameof(ausleiheAssembler));
  }

  public void Raise(IAusleihen ausleihen)
  {
    if ((ausleihen == null) || ausleihen.IsEmpty)
    {
      return;
    }
    businessEventPublisher.PublishOnTransactionComplete<IBuecherAusgeliehenEventService, RaisedNotification>(
      new RaisedNotification
      {
        SessionToken = sessionTokenProvider.Token,
        AusleiheListe = ausleiheAssembler.ToDataContract(ausleihen)
      });
  }
}

Hier wird ein sogenannter BusinessEventPublisher angeschlossen, der ebenfalls eine Transactional Outbox implementiert.

Das nachfolgende Diagramm gibt einen Überblick über die beteiligten Komponenten bei der Zustellung von Business Events, wobei der grobe Ablauf angedeutet ist. Der Windows Dienst Schleupen Business Event Dispatcher entnimmt dabei die Nachricht aus der Queue und ruft die als Subscriber fungierenden Event Service auf.

Aufruf eines asynchronen schreibenden Event-Services

Ohne Resilienzmechanismus, die man durch Verwendung der Transactional Outbox bekommt, kann auch über einen spezifischen Endpunkt das Business Event ausgelöst werden. Dies ist allerdings im Zusammenspiel mit Transaktionen nicht zu empfehlen.

Pattern zur Implementierung eines Subscribers

Ein Subscriber auf ein Business Event wird äquivalent zu einem "normalen" synchronen Service implementiert, da auch dieser über den Business Event Dispatcher aufgerufen wird.

Möglichst hohe Kompatibilität - ein Schleupen.CS-Pattern zur Implementierung

Die Signatur einer Event-Schnittstelle ist dabei im Allgemeinen nur mit Ids versehen, um Events möglichst kompatibel zu halten. Das ist insbesondere bei Events wichtig, da diese in Queues länger verharren können und es bei deutlich verzögerter Zustellung zu Problemen bgzl. der Kompatibilität kommen kann. Daher wird folgendes Pattern implementiert. In diesem Pattern wird dann wiederum ein Gateway analog zu oben implementiert.

Die folgende Abbildung zeigt die übergreifende Umsetzung eines synchronen Event Services - typischerweise um nach einem Event Daten per Entity Service zu lesen und diese zu cachen. Da es in Schleupen.CS wichtig ist, dass insbesondere Events nicht inkompatibel werden, haben Event Services im Allgemeinen nur Ids als Nutzdaten. Um die per Event Service als geändert gemeldeten Daten zu lesen, ist ein Rückaufruf zum Event-auslösenden Baustein mithilfe der Ids notwendig. Dies ist unproblematisch, da die Richtungsabhängigkeit der beteiligten Bausteine hierdurch nicht zerstört wird. Des weiteren kann dann der Subscriber nur die für ihn relevanten Daten lesen.

Das Muster hat grob folgende Gestalt:

Pattern für die Datenübermittlung bei Events

Für jeden benötigten Service-Aufruf - wie im Beispiel grün markiert aus einem Event Service (Subscriber) heraus zum Entity Service - wird im Gateway eine entsprechende Methode implementiert. Hier im Beispiel wird die Methode im Gateway typischerweise QueryById() heißen. Die Schnittstellentypen des Service sollten in der Methode des Gateways nicht nach außen gegeben werden: Ein Gateway hat in seinen Methoden keine Datenverträge, sondern nur Domänenobjekte. Auch hier wird mithilfe eines Assemblers ein Anitcorruption Layer implementiert.

Cookie Consent mit Real Cookie Banner