Die .NET-Entwicklergemeinschaft der Raiffeisenbank setzt die kurze Analyse des Inhalts von ViennaNET fort. Wie und warum wir dazu gekommen sind, .
In diesem Artikel gehen wir auf die noch nicht behandelten Bibliotheken für die Arbeit mit verteilten Transaktionen, Warteschlangen und Datenbanken ein, die in unserem Repository auf GitHub zu finden sind (), und .
ViennaNET.Sagas
Wenn in einem Projekt auf DDD und mikroservicebasierte Architektur umgestellt wird, entsteht beim Verteilen der Geschäftslogik auf verschiedene Services 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 näher vertraut machen, zum Beispiel .
In unseren Projekten haben wir einen einfachen, aber nützlichen Mechanismus implementiert: die Saga, genauer gesagt eine orchestrierte Saga. Ihr Kern ist folgender: Es gibt ein bestimmtes Geschäftsszenario, in dem in verschiedenen Services sequenziell Operationen durchgeführt werden müssen. Falls an irgendeinem Schritt Probleme auftreten, muss ein Rückrollverfahren für alle vorherigen Schritte, wo dies vorgesehen ist, aufgerufen werden. So erhalten wir am Ende der Saga, unabhängig vom Erfolg, konsistente Daten in allen Domänen.
Unsere Implementierung ist derzeit in einer grundlegenden Form und nicht mit der Nutzung irgendwelcher Interaktionsmethoden mit anderen Services verknüpft. Es ist einfach, sie anzuwenden: Man muss lediglich eine Ableitung von der abstrakten Basisklasse SagaBase erstellen, wobei T Ihre Kontextklasse ist, in der die Ausgangsdaten gespeichert werden können, die für die Arbeit mit der Saga benötigt werden, sowie einige Zwischenergebnisse. Eine Instanz des Kontexts wird während der Ausführung an alle Schritte weitergegeben. Die Saga selbst ist eine stateless Klasse, sodass die Instanz als Singleton in die DI eingefügt werden kann, um die notwendigen Abhängigkeiten zu erhalten.
Beispiel für die Deklaration:
public class ExampleSaga : SagaBase<ExampleContext>
{
public ExampleSaga()
{
Step("Schritt 1")
.WithAction(c => ...)
.WithCompensation(c => ...);
AsyncStep("Schritt 2")
.WithAction(async c => ...);
}
}
Beispielaufruf:
var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);
Vollständige Beispiele für verschiedene Implementierungen finden Sie auch in der Sammlung mit .
ViennaNET.Orm.*
Ein Set von Bibliotheken zur Arbeit mit verschiedenen Datenbanken über Nhibernate. Wir verwenden einen DB-First-Ansatz mit Liquibase, daher ist hier nur die Funktionalität zur Arbeit mit Daten in einer vorhandenen Datenbank enthalten.
ViennaNET.Orm.Seedwork und ViennaNET.Orm – die Hauptbibliotheken, die die grundlegenden Schnittstellen und deren Implementierungen enthalten. Lassen Sie uns näher auf deren Inhalte eingehen.
Benutzeroberfläche IEntityFactoryService und seine Implementierung EntityFactoryService sind der Hauptanlaufpunkt für die Arbeit mit der Datenbank, da hier das Unit of Work, die Repositories zur Arbeit mit spezifischen Entitäten sowie die Ausführenden von Kommandos und direkten SQL-Abfragen erstellt werden. Manchmal ist es sinnvoll, die Möglichkeiten der Klasse zur Arbeit mit der Datenbank zu beschränken, beispielsweise nur Lesezugriff auf Daten zu gewähren. Für solche Fälle gibt es IEntityFactoryService einen Vorgänger – die Schnittstelle IEntityRepositoryFactory, in der nur die Methode zur Erstellung von Repositories deklariert ist.
Für den direkten Zugriff auf die Datenbank kommt ein Provider-Mechanismus zum Einsatz. Jede verwendete Datenbankmanagementsystem (DBMS) hat ihre eigene Implementierung in unseren Teams: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.
In einer Anwendung können mehrere Provider gleichzeitig registriert sein, was beispielsweise eine schrittweise Migration von einem DBMS zu einem anderen innerhalb eines Dienstes ermöglicht, ohne dass zusätzliche Infrastrukturkosten anfallen. Der Mechanismus zur Auswahl der benötigten Verbindung und damit des Providers für eine bestimmte Entität (für die das Mapping zu den DB-Tabellen geschrieben wird) wird durch die Registrierung der Entität in der Klasse BoundedContext (beinhaltet eine Methode zur Registrierung von Domänenentitäten) oder deren Nachfolger ApplicationContext (beinhaltet Methoden zur Registrierung von Anwendungsentitäten, direkten Anfragen und Befehlen) realisiert, wobei als Argument die Verbindungs-ID aus der Konfiguration übergeben wird:
"db": [
{
"nick": "mssql_connection",
"dbServerType": "MSSQL",
"ConnectionString": "...",
"useCallContext": true
},
{
"nick": "oracle_connection",
"dbServerType": "Oracle",
"ConnectionString": "..."
}
],
Beispiel für ApplicationContext:
interne versiegelte Klasse DbContext : ApplicationContext
{
public DbContext()
{
AddEntity("mssql_connection");
AddEntity("oracle_connection");
AddEntity("oracle_connection");
}
}
Wenn keine Verbindungs-ID angegeben ist, wird die Verbindung mit dem Namen „default“ verwendet.
Die Zuordnung von Entitäten zu Datenbanktabellen erfolgt mit den Standardmitteln von NHibernate. Die Beschreibung kann sowohl über XML-Dateien als auch über Klassen erfolgen. Für das bequeme Schreiben von Stubs in Unit-Tests steht eine Bibliothek zur Verfügung. ViennaNET.TestUtils.Orm.
Vollständige Beispiele zur Verwendung von ViennaNET.Orm.* finden Sie unter .
ViennaNET.Messaging.*
eine Sammlung von Bibliotheken zur Arbeit mit Warteschlangen.
Für die Arbeit mit Warteschlangen wurde der gleiche Ansatz gewählt wie bei den verschiedenen DBMS, nämlich ein möglichst einheitlicher Ansatz in Bezug auf die Bibliotheksnutzung, unabhängig vom verwendeten Warteschlangenmanager. Die Bibliothek ViennaNET.Messaging ist genau für diese Vereinheitlichung verantwortlich, und ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue und ViennaNET.Messaging.KafkaQueue enthält Implementierungen von Adaptern für IBM MQ, RabbitMQ und Kafka.
Beim Arbeiten mit Warteschlangen gibt es zwei Prozesse: Empfang von Nachrichten und Versand.
Betrachten wir den Empfang. Es gibt hier zwei Optionen: für das kontinuierliche Abhören und für den Empfang einer einzelnen Nachricht. Für das kontinuierliche Abhören einer Warteschlange müssen Sie zuerst die Prozessor-Klasse beschreiben, die von IMessageProcessorerbt und für die Verarbeitung der eingehenden Nachricht zuständig sein wird. Anschließend muss diese an eine bestimmte Warteschlange „gebunden“ werden, was durch die Registrierung in IQueueReactorFactory mit der Angabe der Warteschlangen-ID aus der Konfiguration geschieht:
"messaging": {
"ApplicationName": "MyApplication"
},
"rabbitmq": {
"queues": [
{
"id": "myQueue",
"queuename": "lalala",
...
}
]
},
Beispiel für den Start des Abhörens:
_queueReactorFactory.Register<MyMessageProcessor>("myQueue");
var queueReactor = queueReactorFactory.CreateQueueReactor("myQueue");
queueReactor.StartProcessing();
Dann werden beim Starten des Dienstes und beim Aufrufen der Methode zum Beginn des Abhörens alle Nachrichten aus der angegebenen Warteschlange an den entsprechenden Prozessor übergeben.
Für den Empfang einer einzelnen Nachricht im Fabrik-Interface IMessagingComponentFactory gibt es die Methode CreateMessageReceiver, die einen Empfänger erstellt, der auf Nachrichten aus der ihm zugewiesenen Warteschlange wartet:
using (var receiver = _messagingComponentFactory.CreateMessageReceiver<TestMessage>("myQueue"))
{
var message = receiver.Receive();
}
Um eine Nachricht zu senden Sie müssen dasselbe nutzen IMessagingComponentFactory und einen Nachrichtensender erstellen:
using (var sender = _messagingComponentFactory.CreateMessageSender("myQueue"))
{
sender.SendMessage(new MyMessage { Value = ...});
}
Für die Serialisierung und Deserialisierung von Nachrichten stehen drei vorgefertigte Varianten zur Verfügung: reiner Text, XML und JSON, aber bei Bedarf können Sie problemlos eigene Implementierungen der Schnittstellen IMessageSerializer und IMessageDeserializer erstellen.
Wir haben uns bemüht, die einzigartigen Fähigkeiten jedes Queue-Managers beizubehalten, zum Beispiel ViennaNET.Messaging.MQSeriesQueue die es ermöglicht, nicht nur Text-, sondern auch Byte-Nachrichten zu senden, während ViennaNET.Messaging.RabbitMQQueue Routing und die Erstellung von Warteschlangen „on the fly“ unterstützt. In unserem Adapter für RabbitMQ haben wir auch eine Art RPC implementiert: wir senden eine Nachricht und erwarten eine Antwort aus einer speziellen temporären Warteschlange, die nur für eine Antwortnachricht erstellt wird.
Hier .
ViennaNET.CallContext
Wir nutzen Warteschlangen nicht nur für die Integration zwischen verschiedenen Systemen, sondern auch für die Kommunikation zwischen den Mikrodiensten einer Anwendung, etwa im Rahmen einer Saga. Dies führte zu der Notwendigkeit, zusammen mit der Nachricht Hilfsdaten wie den Benutzernamen, die Anforderungs-ID für das durchgängige Logging, die IP-Adresse des Quellursprungs und Authentifizierungsdaten zu übertragen. Zur Umsetzung dieser Datenübertragung haben wir eine Bibliothek entwickelt, ViennaNET.CallContext, die es ermöglicht, Daten aus der eingehenden Anfrage an den Dienst zu speichern. Dabei spielt es keine Rolle, wie die Anfrage gestellt wurde - ob über eine Warteschlange oder über HTTP. Vor dem Versenden der ausgehenden Anfrage oder Nachricht werden die Daten aus dem Kontext abgerufen und in die Header eingefügt. So erhält der nächste Dienst die Hilfsdaten und kann entsprechend damit umgehen.
Vielen Dank für Ihre Aufmerksamkeit, wir freuen uns auf Ihre Kommentare und Pull Requests!
Quelle: habr.com
