In diesem Artikel werden wir die noch nicht berücksichtigten Bibliotheken für die Arbeit mit verteilten Transaktionen, Warteschlangen und Datenbanken durchgehen, die sich in unserem Repository auf GitHub befinden (die Quelle ist hier ), und Nuget-Pakete sind hier .
ViennaNET.Sagas
Wenn ein Projekt auf DDD und eine Microservice-Architektur umgestellt wird und die Geschäftslogik auf verschiedene Services verteilt ist, tritt ein Problem auf, das mit der Notwendigkeit verbunden ist, den Mechanismus verteilter Transaktionen zu implementieren, da viele Szenarien häufig mehrere Domänen gleichzeitig betreffen. Weitere Informationen zu solchen Mechanismen finden Sie beispielsweise im Buch "Microservices Patterns" von Chris Richardson .
In unseren Projekten haben wir einen einfachen, aber nützlichen Mechanismus implementiert: eine Saga oder vielmehr eine auf Orchestrierung basierende Saga. Das Wesentliche ist wie folgt: Es gibt ein bestimmtes Geschäftsszenario, in dem es erforderlich ist, Operationen in verschiedenen Diensten nacheinander auszuführen, während bei Problemen in einem Schritt das Rollback-Verfahren aller vorherigen Schritte aufgerufen werden muss, in denen es bereitgestellt wird. So erhalten wir am Ende der Saga unabhängig vom Erfolg konsistente Daten über alle Domänen hinweg.
Unsere Implementierung ist immer noch grundlegend und nicht an die Verwendung von Interaktionsmethoden mit anderen Diensten gebunden. Es ist nicht schwierig, es zu verwenden: Es reicht aus, von der abstrakten Basisklasse SagaBase <T> zu erben, wobei T Ihre Kontextklasse ist, in der Sie die anfänglichen Daten speichern können, die für das Funktionieren der Saga erforderlich sind, sowie einige Zwischenergebnisse. Die Kontextinstanz wird zur Laufzeit an alle Schritte weitergeleitet. Die Saga selbst ist eine zustandslose Klasse, daher kann die Instanz als Singleton im DI platziert werden, um die erforderlichen Abhängigkeiten zu erhalten.
Beispieldeklaration:
public class ExampleSaga : SagaBase<ExampleContext>
{
public ExampleSaga()
{
Step("Step 1")
.WithAction(c => ...)
.WithCompensation(c => ...);
AsyncStep("Step 2")
.WithAction(async c => ...);
}
}
Beispiel aufrufen:
var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);
Vollständige Beispiele für verschiedene Implementierungen finden Sie hier und in der Baugruppe mit Tests .
ViennaNET.Orm. *
Eine Reihe von Bibliotheken für die Arbeit mit verschiedenen Datenbanken über Nhibernate. Wir verwenden den DB-First-Ansatz unter Verwendung von Liquibase, daher gibt es nur Funktionen für die Arbeit mit Daten in der fertigen Datenbank.
ViennaNET.Orm.Seedwork ViennaNET.Orm- Hauptbaugruppen mit Basisschnittstellen bzw. deren Implementierungen. Lassen Sie uns näher auf ihren Inhalt eingehen.
Die Schnittstelle
IEntityFactoryServiceund ihre Implementierung EntityFactoryServicesind der Hauptstartpunkt für die Arbeit mit der Datenbank, da hier die Arbeitseinheit, Repositorys für die Arbeit mit bestimmten Entitäten sowie Ausführende von Befehlen und direkten SQL-Abfragen erstellt werden. Manchmal ist es zweckmäßig, die Funktionen einer Klasse für die Arbeit mit einer Datenbank einzuschränken, um beispielsweise schreibgeschützte Daten zu aktivieren. In solchen Fällen hat es IEntityFactoryServiceeinen Vorfahren - eine Schnittstelle, IEntityRepositoryFactoryin der nur die Methode zum Erstellen von Repositorys deklariert ist.
Der Provider-Mechanismus wird verwendet, um direkt auf die Datenbank zuzugreifen. Für jeden in unserer Datenbank verwendeten haben Teams ihre eigene Implementierung :
ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.
Gleichzeitig können mehrere Anbieter gleichzeitig in einer Anwendung registriert werden, was beispielsweise im Rahmen eines Dienstes ohne Kosten für die Aktualisierung der Infrastruktur eine schrittweise Migration von einem DBMS zu einem anderen ermöglicht. Der Mechanismus zum Auswählen der erforderlichen Verbindung und damit des Anbieters für eine bestimmte Entitätsklasse (für die die Zuordnung zu den Datenbanktabellen geschrieben wird) wird durch die Registrierung der Entität in der BoundedContext-Klasse (enthält eine Methode zum Registrieren von Domänenentitäten) oder ihres Nachfolgers ApplicationContext (enthält Methoden zum Registrieren von Anwendungsentitäten) implementiert , direkte Anforderungen und Befehle), wobei die Verbindungskennung aus der Konfiguration als Argument verwendet wird:
"db": [
{
"nick": "mssql_connection",
"dbServerType": "MSSQL",
"ConnectionString": "...",
"useCallContext": true
},
{
"nick": "oracle_connection",
"dbServerType": "Oracle",
"ConnectionString": "..."
}
],
Anwendungskontext-Beispiel:
internal sealed class DbContext : ApplicationContext
{
public DbContext()
{
AddEntity<SomeEntity>("mssql_connection");
AddEntity<MigratedSomeEntity>("oracle_connection");
AddEntity<AnotherEntity>("oracle_connection");
}
}
Wenn keine Verbindungskennung angegeben ist, wird die Verbindung mit dem Namen "Standard" verwendet.
Die direkte Zuordnung von Entitäten zu Datenbanktabellen wird mithilfe von Standard-NHibernate-Tools implementiert. Sie können die Beschreibung sowohl über XML-Dateien als auch über Klassen verwenden. Zum bequemen Schreiben von Stub-Repositorys in Unit-Tests gibt es eine Bibliothek
ViennaNET.TestUtils.Orm.
Vollständige Beispiele für die Verwendung von ViennaNET.Orm. * Hier finden Sie .
ViennaNET.Messaging. *
Eine Reihe von Bibliotheken zum Arbeiten mit Warteschlangen.
Für die Arbeit mit Warteschlangen wurde der gleiche Ansatz gewählt wie für verschiedene DBMS, nämlich der maximal mögliche einheitliche Ansatz für die Arbeit mit der Bibliothek, unabhängig vom verwendeten Warteschlangenmanager. Die Bibliothek
ViennaNET.Messagingist nur für diese Vereinheitlichung verantwortlich und ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue ViennaNET.Messaging.KafkaQueueenthält die Adapterimplementierungen für IBM MQ, RabbitMQ bzw. Kafka.
Bei der Arbeit mit Warteschlangen gibt es zwei Prozesse: Empfangen einer Nachricht und Senden.
Erwägen Sie zu bekommen. Hier gibt es zwei Möglichkeiten: zum ständigen Abhören und zum Empfangen einer einzelnen Nachricht. Um die Warteschlange ständig abzuhören, müssen Sie zunächst die Prozessorklasse beschreiben, von der geerbt wurde
IMessageProcessor, die für die Verarbeitung der eingehenden Nachricht verantwortlich ist. Außerdem muss es an eine bestimmte Warteschlange "gebunden" sein. Dies erfolgt durch Registrieren IQueueReactorFactorymit Angabe der Warteschlangenkennung aus der Konfiguration:
"messaging": {
"ApplicationName": "MyApplication"
},
"rabbitmq": {
"queues": [
{
"id": "myQueue",
"queuename": "lalala",
...
}
]
},
Ein Beispiel für den Beginn des Zuhörens:
_queueReactorFactory.Register<MyMessageProcessor>("myQueue");
var queueReactor = queueReactorFactory.CreateQueueReactor("myQueue");
queueReactor.StartProcessing();
Wenn der Dienst gestartet wird und die Methode zum Abhören aufgerufen wird, werden alle Nachrichten aus der angegebenen Warteschlange an den entsprechenden Prozessor gesendet.
Um eine einzelne Nachricht in der Factory-Oberfläche zu empfangen,
IMessagingComponentFactorygibt es eine Methode CreateMessageReceiver, mit der ein Empfänger erstellt wird, der auf eine Nachricht aus der angegebenen Warteschlange wartet:
using (var receiver = _messagingComponentFactory.CreateMessageReceiver<TestMessage>("myQueue"))
{
var message = receiver.Receive();
}
Um eine Nachricht zu senden, müssen Sie dieselbe verwenden
IMessagingComponentFactoryund einen Absender der Nachricht erstellen:
using (var sender = _messagingComponentFactory.CreateMessageSender<MyMessage>("myQueue"))
{
sender.SendMessage(new MyMessage { Value = ...});
}
Es gibt drei vorgefertigte Optionen zum Serialisieren und Deserialisieren einer Nachricht: nur Text, XML und JSON. Bei Bedarf können Sie die Schnittstellen jedoch sicher selbst implementieren
IMessageSerializer IMessageDeserializer.
Wir haben versucht, die einzigartigen Funktionen jedes Warteschlangenmanagers beizubehalten. So können beispielsweise
ViennaNET.Messaging.MQSeriesQueuenicht nur Textnachrichten, sondern auch Byte-Nachrichten gesendet und ViennaNET.Messaging.RabbitMQQueueRouting und Warteschlangen im laufenden Betrieb unterstützt werden. Unser Adapter-Wrapper für RabbitMQ implementiert auch einen Anschein von RPC: Wir senden eine Nachricht und warten auf eine Antwort aus einer speziellen temporären Warteschlange, die nur für eine Antwortnachricht erstellt wird.
Hier ist ein Beispiel für die Verwendung von Warteschlangen mit grundlegenden Verbindungsnuancen .
ViennaNET.CallContext
Wir verwenden Warteschlangen nicht nur für die Integration zwischen verschiedenen Systemen, sondern auch für die Kommunikation zwischen Mikrodiensten einer Anwendung, beispielsweise im Rahmen einer Saga. Dies führte dazu, dass zusammen mit der Nachricht Zusatzdaten wie Benutzername, Anforderungs-ID für die End-to-End-Protokollierung, Quell-IP-Adresse und Autorisierungsdaten übertragen werden mussten. Um die Weiterleitung dieser Daten zu implementieren, wurde eine Bibliothek entwickelt
ViennaNET.CallContext, in der Daten aus einer Anforderung gespeichert werden können, die in den Dienst eingeht. In diesem Fall spielt es keine Rolle, wie die Anforderung über die Warteschlange oder über HTTP gestellt wurde. Vor dem Senden einer ausgehenden Anforderung oder Nachricht werden die Daten dann aus dem Kontext entfernt und in die Header eingefügt. Somit empfängt der nächste Dienst Hilfsdaten und entsorgt diese auf die gleiche Weise.
Vielen Dank für Ihre Aufmerksamkeit, wir freuen uns auf Ihre Kommentare und Pull-Anfragen!