ViennaNET: Bibliothekenpaket fĂŒr das Backend. Teil 2

Die .NET-Entwicklergemeinschaft der Raiffeisenbank setzt ihre kurze Analyse des Inhalts von ViennaNET fort. Wie und warum wir dazu gekommen sind, kann man im ersten Teil nachlesen.

In diesem Artikel beschĂ€ftigen wir uns mit den noch nicht behandelten Bibliotheken fĂŒr die Arbeit mit verteilten Transaktionen, Warteschlangen und Datenbanken, die in unserem Repository auf GitHub zu finden sind (Der Quellcode befindet sich hier), und Nuget-Pakete sind hier.

ViennaNET: Bibliothekenpaket fĂŒr das Backend. Teil 2

ViennaNET.Sagas

Wenn in einem Projekt der Übergang zu DDD und einer Mikroservice-Architektur stattfindet, entsteht beim Verteilen der GeschĂ€ftslogik auf verschiedene Dienste das Problem, einen Mechanismus fĂŒr verteilte Transaktionen zu implementieren, da viele Szenarien hĂ€ufig mehrere DomĂ€nen betreffen. Mit solchen Mechanismen kann man sich beispielsweise besser vertrautmachen, in dem Buch „Microservices Patterns“, Chris Richardson.

In unseren Projekten haben wir einen einfachen, aber nĂŒtzlichen Mechanismus implementiert: eine Saga, genauer gesagt eine Saga auf der Basis von Orchestrierung. Der Kern davon ist Folgendes: Es gibt ein bestimmtes GeschĂ€ftsszenario, in dem nacheinander Operationen in verschiedenen Diensten durchgefĂŒhrt werden mĂŒssen, wobei im Falle von Problemen in einem der Schritte eine RĂŒckgĂ€ngig-Machungsprozedur fĂŒr alle vorhergehenden Schritte aufgerufen werden muss, sofern dies vorgesehen ist. So erhalten wir am Ende der AusfĂŒhrung der Saga, unabhĂ€ngig vom Erfolg, konsistente Daten in allen DomĂ€nen.

Unsere Implementierung ist derzeit in einfacher Form und nicht an die Verwendung bestimmter Interaktionsmethoden mit anderen Diensten gebunden. Es ist nicht schwierig, sie anzuwenden: Man muss lediglich von der abstrakten Basisklasse SagaBase erben, wobei T Ihre Kontextklasse ist, in der die erforderlichen Ausgangsdaten fĂŒr die Arbeit mit der Saga sowie einige Zwischenresultate gespeichert werden können. Eine Instanz des Kontextes wird wĂ€hrend der AusfĂŒhrung an alle Schritte weitergereicht. Die Saga selbst ist eine stateless Klasse, sodass die Instanz als Singleton in DI eingefĂŒgt werden kann, um die erforderlichen AbhĂ€ngigkeiten zu erhalten.

Beispiel der Deklaration:

public class ExampleSaga : SagaBase
{
  public ExampleSaga()
  {
    Step("Schritt 1")
      .WithAction(c => ...)
      .WithCompensation(c => ...);
	
    AsyncStep("Schritt 2")
      .WithAction(async c => ...);
  }
}

Beispiel fĂŒr den Aufruf:

var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);

VollstÀndige Beispiele verschiedener Implementierungen finden Sie hier auch in der Sammlung mit Tests.

ViennaNET.Orm.*

Eine Sammlung von Bibliotheken zur Arbeit mit verschiedenen Datenbanken ĂŒber Nhibernate. Wir verwenden den DB-First-Ansatz mit Liquibase, daher ist hier nur die FunktionalitĂ€t zur Arbeit mit Daten in einer fertigen Datenbank vorhanden.

ViennaNET.Orm.Seedwork und ViennaNET.Orm sind die Hauptassemblies, die die grundlegenden Schnittstellen und deren Implementierungen enthalten. Lassen Sie uns genauer auf deren Inhalt eingehen.

BenutzeroberflĂ€che IEntityFactoryService und dessen Implementierung EntityFactoryService sind der Hauptansatzpunkt fĂŒr die Arbeit mit der Datenbank, da hier das Unit of Work, die Repositories zur Arbeit mit bestimmten EntitĂ€ten sowie die AusfĂŒhrenden von Befehlen und direkten SQL-Abfragen erstellt werden. Manchmal ist es praktisch, die Möglichkeiten der Klasse zur Arbeit mit der Datenbank einzuschrĂ€nken, z. B. um nur Lesezugriff auf Daten zu gewĂ€hren. FĂŒr solche FĂ€lle hat IEntityFactoryService einen VorgĂ€nger – die Schnittstelle IEntityRepositoryFactory, in der nur die Methode zum Erstellen von Repositories deklariert ist.

FĂŒr den direkten Zugriff auf die Datenbank wird der Provider-Mechanismus verwendet. FĂŒr jede in unseren Teams verwendete Datenbank-Management-System (DBMS) gibt es eine eigene Implementierung: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Dabei können in einer Anwendung mehrere Provider gleichzeitig registriert sein, was es zum Beispiel ermöglicht, innerhalb eines Dienstes ohne zusĂ€tzliche Infrastrukturkosten schrittweise von einem DBMS zu einem anderen zu migrieren. Der Mechanismus zur Auswahl der erforderlichen Verbindung und folglich des Providers fĂŒr eine bestimmte EntitĂ€tsklasse (fĂŒr die das Mapping auf Datenbanktabellen geschrieben wird) wird durch die Registrierung der EntitĂ€t in der Klasse BoundedContext (enthĂ€lt eine Methode zur Registrierung von DomĂ€nenentitĂ€ten) oder dessen Nachfolger ApplicationContext (enthĂ€lt Methoden zur Registrierung von AnwendungsentitĂ€ten, direkten Abfragen und Befehlen) realisiert, wobei der Identifier der Verbindung aus der Konfiguration ĂŒbergeben wird:

"db": [
  {
    "nick": "mssql_connection",
    "dbServerType": "MSSQL",
    "ConnectionString": "...",
    "useCallContext": true
  },
  {
    "nick": "oracle_connection",
    "dbServerType": "Oracle",
    "ConnectionString": "..."
  }
],

Beispiel fĂŒr ApplicationContext:

internal sealed class DbContext : ApplicationContext
{
  public DbContext()
  {
    AddEntity("mssql_connection");
    AddEntity("oracle_connection");
    AddEntity("oracle_connection");
  }
}

Wenn der Verbindung-Identifier nicht angegeben ist, wird die Verbindung mit dem Namen „default“ verwendet.

Die Zuordnung von EntitĂ€ten zu Datenbanktabellen erfolgt mit den standardmĂ€ĂŸigen Mitteln von NHibernate. Es können sowohl XML-Dateien als auch Klassen verwendet werden. FĂŒr die bequeme Erstellung von Stub-Repositories in Unit-Tests steht eine Bibliothek zur VerfĂŒgung. ViennaNET.TestUtils.Orm.

Vollwertige Anwendungsbeispiele fĂŒr ViennaNET.Orm.* finden Sie hier.

ViennaNET.Messaging.*

Ein Satz von Bibliotheken zur Arbeit mit Warteschlangen.

FĂŒr die Arbeit mit Warteschlangen wurde der gleiche Ansatz wie bei verschiedenen DBMS gewĂ€hlt, nĂ€mlich ein maximal möglicher einheitlicher Ansatz in Bezug auf die Arbeit mit der Bibliothek, unabhĂ€ngig vom verwendeten Warteschlangen-Manager. Die Bibliothek ViennaNET.Messaging verantwortet genau diese Vereinheitlichung, und ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue und ViennaNET.Messaging.KafkaQueue enthalten Implementierungen von Adaptern fĂŒr IBM MQ, RabbitMQ und Kafka.

Bei der Arbeit mit Warteschlangen gibt es zwei Prozesse: das Empfangen von Nachrichten und das Senden.

Betrachten wir das Empfangen. Hier gibt es 2 Optionen: fĂŒr das stĂ€ndige Abhören und fĂŒr das Empfangen einer einzelnen Nachricht. FĂŒr das stĂ€ndige Abhören einer Warteschlange muss zunĂ€chst eine Prozessor-Klasse beschrieben werden, die von IMessageProcessorerbt und fĂŒr die Verarbeitung eingehender Nachrichten zustĂ€ndig ist. Danach muss sie "an eine bestimmte Warteschlange gebunden" werden, was durch die Registrierung in IQueueReactorFactory unter Angabe der Warteschlangen-ID aus der Konfiguration erfolgt:

"messaging": {
    "ApplicationName": "MyApplication"
},
"rabbitmq": {
    "queues": [
      {
        "id": "myQueue",
        "queuename": "lalala",
        ...
      }
    ]
},

Beispiel fĂŒr den Start des Abhörens:

_queueReactorFactory.Register("myQueue");
var queueReactor = queueReactorFactory.CreateQueueReactor("myQueue");
queueReactor.StartProcessing();

Somit gelangen beim Start des Dienstes und beim Aufruf der Methode zum Starten des Abhörens alle Nachrichten aus der angegebenen Warteschlange in den entsprechenden Prozessor.

FĂŒr das Empfangen einer einzelnen Nachricht gibt es in der Fabrik-Schnittstelle IMessagingComponentFactory eine Methode CreateMessageReceiver, die einen EmpfĂ€nger erstellt, der auf Nachrichten aus der angegebenen Warteschlange wartet:

using (var receiver = _messagingComponentFactory.CreateMessageReceiver("myQueue"))
{
    var message = receiver.Receive();
}

Um eine Nachricht zu senden, muss die gleiche IMessagingComponentFactory verwendet werden, um einen Nachrichtenversender zu erstellen:

using (var sender = _messagingComponentFactory.CreateMessageSender("myQueue"))
{
    sender.SendMessage(new MyMessage { Value = ...});
}

FĂŒr die Serialisierung und Deserialisierung von Nachrichten gibt es drei fertige Varianten: einfachen Text, XML und JSON. Bei Bedarf kann man auch eigene Implementierungen der Schnittstellen erstellen. IMessageSerializer und IMessageDeserializer.

Wir haben uns bemĂŒht, die einzigartigen Möglichkeiten jedes Message-Queue-Managers zu bewahren, zum Beispiel ViennaNET.Messaging.MQSeriesQueue erlaubt das Senden von nicht nur Text-, sondern auch Byte-Nachrichten, wĂ€hrend ViennaNET.Messaging.RabbitMQQueue Routing und die Erstellung von Queues "on-the-fly" unterstĂŒtzt. In unserem Adapter fĂŒr RabbitMQ gibt es auch eine Art RPC-Implementation: Wir senden eine Nachricht und warten auf eine Antwort aus einer speziellen temporĂ€ren Queue, die nur fĂŒr eine einzelne Antwortnachricht erstellt wird.

Hier ein Beispiel fĂŒr die Verwendung von Queues mit den wichtigsten Anbindungsnuancen.

ViennaNET.CallContext

Wir verwenden Queues nicht nur zur Integration zwischen verschiedenen Systemen, sondern auch zur Kommunikation zwischen Microservices einer Anwendung, zum Beispiel im Rahmen einer Saga. Dies hat zur Notwendigkeit gefĂŒhrt, zusĂ€tzliche Daten wie den Benutzernamen, die Request-ID fĂŒr durchgĂ€ngiges Logging, die IP-Adresse des Senders und die Authentifizierungsdaten zusammen mit der Nachricht zu ĂŒbertragen. Um diese Daten durchzuschleusen, haben wir eine Bibliothek entwickelt, ViennaNET.CallContext, die es ermöglicht, Daten aus dem eingehenden Service-Request zu speichern. Dabei spielt es keine Rolle, wie die Anfrage gemacht wurde, ob ĂŒber eine Queue oder ĂŒber HTTP. Dann werden vor dem Senden der ausgehenden Anfrage oder Nachricht die Daten aus dem Kontext abgerufen und in die Header eingefĂŒgt. So erhĂ€lt der nĂ€chste Service die zusĂ€tzlichen Daten und kann sie entsprechend verwalten.

Danke fĂŒr Ihre Aufmerksamkeit, wir freuen uns auf Ihre Kommentare und Pull Requests!

Quelle: habr.com

60GB SSD 8Gb DDR4