ViennaNET: eine Reihe von Bibliotheken für das Backend. Teil 2

Die Raiffeisenbank .NET-Entwicklergemeinschaft prüft weiterhin kurz die Inhalte von ViennaNET. Darüber, wie und warum wir dazu gekommen sind, Sie können den ersten Teil lesen.

In diesem Artikel gehen wir auf noch zu berücksichtigende Bibliotheken für die Arbeit mit verteilten Transaktionen, Warteschlangen und Datenbanken ein, die in unserem GitHub-Repository zu finden sind (Quellen gibt es hier), und Nuget-Pakete hier.

ViennaNET: eine Reihe von Bibliotheken für das Backend. Teil 2

ViennaNET.Sagas

Wenn ein Projekt auf DDD und Microservice-Architektur umsteigt und die Geschäftslogik auf verschiedene Dienste verteilt ist, entsteht ein Problem im Zusammenhang mit der Notwendigkeit, einen verteilten Transaktionsmechanismus zu implementieren, da viele Szenarien oft mehrere Domänen gleichzeitig betreffen. Sie können sich beispielsweise mit solchen Mechanismen genauer vertraut machen, im Buch „Microservices Patterns“, Chris Richardson.

In unseren Projekten haben wir einen einfachen, aber nützlichen Mechanismus implementiert: eine Saga, oder besser gesagt eine auf Orchestrierung basierende Saga. Sein Kern ist wie folgt: Es gibt ein bestimmtes Geschäftsszenario, in dem es notwendig ist, Vorgänge in verschiedenen Diensten nacheinander auszuführen, und wenn bei irgendeinem Schritt Probleme auftreten, ist es notwendig, die Rollback-Prozedur für alle vorherigen Schritte aufzurufen, wo sie sich befindet bereitgestellt. So erhalten wir am Ende der Saga unabhängig vom Erfolg konsistente Daten in allen Bereichen.

Unsere Implementierung erfolgt weiterhin in ihrer Grundform und ist nicht an die Verwendung irgendwelcher Interaktionsmethoden mit anderen Diensten gebunden. Die Verwendung ist nicht schwierig: Erstellen Sie einfach einen Nachkommen der abstrakten Basisklasse SagaBase<T>, wobei T Ihre Kontextklasse ist, in der Sie die für das Funktionieren der Saga erforderlichen Anfangsdaten sowie einige Zwischenergebnisse speichern können. Die Kontextinstanz wird während der Ausführung an alle Schritte weitergegeben. Saga selbst ist eine zustandslose Klasse, sodass die Instanz als Singleton in DI platziert werden kann, um die erforderlichen Abhängigkeiten zu erhalten.

Beispielanzeige:

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

Beispielaufruf:

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

Vollständige Beispiele verschiedener Implementierungen können angezeigt werden hier und in Montage mit Tests.

WienNET.Orm.*

Eine Reihe von Bibliotheken für die Arbeit mit verschiedenen Datenbanken über Nhibernate. Wir verwenden den DB-First-Ansatz mit Liquibase, daher gibt es nur Funktionen für die Arbeit mit Daten in einer vorgefertigten Datenbank.

ViennaNET.Orm.Seedwork и ViennaNET.Orm – Hauptassemblys, die grundlegende Schnittstellen bzw. deren Implementierungen enthalten. Schauen wir uns ihren Inhalt genauer an.

Schnittstelle IEntityFactoryService und seine Umsetzung EntityFactoryService sind der Hauptausgangspunkt für die Arbeit mit der Datenbank, da hier die Unit of Work, Repositorys für die Arbeit mit bestimmten Entitäten sowie Ausführer von Befehlen und direkten SQL-Abfragen erstellt werden. Manchmal ist es praktisch, die Fähigkeiten einer Klasse für die Arbeit mit einer Datenbank einzuschränken, um beispielsweise die Möglichkeit bereitzustellen, nur Daten zu lesen. Für solche Fälle IEntityFactoryService Es gibt eine Vorfahren-Schnittstelle IEntityRepositoryFactory, das lediglich eine Methode zum Erstellen von Repositorys deklariert.

Um direkt auf die Datenbank zuzugreifen, wird der Provider-Mechanismus verwendet. Jedes DBMS, das wir in unseren Teams verwenden, verfügt über eine 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 eine schrittweise Migration ohne Kosten für die Änderung der Infrastruktur ermöglicht von einem DBMS zum anderen. Der Mechanismus zur Auswahl der erforderlichen Verbindung und damit des Anbieters für eine bestimmte Entitätsklasse (für die die Zuordnung zu Datenbanktabellen geschrieben wird) wird durch die Registrierung der Entität in der BoundedContext-Klasse (enthält eine Methode zur Registrierung von Domänenentitäten) oder deren Nachfolger implementiert ApplicationContext (enthält Methoden zur Registrierung von Anwendungsentitäten, direkten Anfragen und Befehlen), wobei die Verbindungskennung aus der Konfiguration als Argument akzeptiert wird:

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

Beispiel-Anwendungskontext:

internal sealed class DbContext : ApplicationContext
{
  public DbContext()
  {
    AddEntity<SomeEntity>("mssql_connection");
    AddEntity<MigratedSomeEntity>("oracle_connection");
    AddEntity<AnotherEntity>("oracle_connection");
  }
}

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

Die direkte Zuordnung von Entitäten zu Datenbanktabellen wird mithilfe von NHibernate-Standardtools implementiert. Sie können die Beschreibung sowohl über XML-Dateien als auch über Klassen verwenden. Zum bequemen Schreiben von Stub-Repositories in Unit-Tests gibt es eine Bibliothek ViennaNET.TestUtils.Orm.

Vollständige Beispiele für die Verwendung von ViennaNET.Orm.* finden Sie hier hier.

ViennaNET.Messaging.*

Eine Reihe von Bibliotheken zum Arbeiten mit Warteschlangen.

Für die Arbeit mit Warteschlangen wurde der gleiche Ansatz wie bei verschiedenen DBMS gewählt, nämlich der größtmögliche einheitliche Ansatz hinsichtlich der Arbeit mit der Bibliothek, unabhängig vom verwendeten Warteschlangenmanager. Bibliothek ViennaNET.Messaging ist genau für diese Vereinigung verantwortlich, und ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue и ViennaNET.Messaging.KafkaQueue enthalten Adapterimplementierungen für IBM MQ, RabbitMQ bzw. Kafka.

Bei der Arbeit mit Warteschlangen gibt es zwei Prozesse: den Empfang einer Nachricht und den Versand.

Erwägen Sie den Empfang. Hier gibt es 2 Möglichkeiten: zum kontinuierlichen Abhören und zum Empfang einer einzelnen Nachricht. Um die Warteschlange ständig abzuhören, müssen Sie zunächst die Prozessorklasse beschreiben, von der geerbt wurde IMessageProcessor, der für die Verarbeitung der eingehenden Nachricht verantwortlich ist. Als nächstes muss es mit einer bestimmten Warteschlange „verknüpft“ werden; dies geschieht durch die Registrierung in IQueueReactorFactory Angabe der Warteschlangenkennung aus der Konfiguration:

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

Beispiel für den Beginn des Zuhörens:

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

Wenn dann der Dienst startet und die Methode zum Starten des Abhörens aufgerufen wird, werden alle Nachrichten aus der angegebenen Warteschlange an den entsprechenden Prozessor weitergeleitet.

Zum Empfangen einer einzelnen Nachricht in einer Factory-Schnittstelle IMessagingComponentFactory Es gibt eine Methode CreateMessageReceiverDadurch wird ein Empfänger erstellt, der auf eine Nachricht aus der angegebenen Warteschlange wartet:

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

Eine Nachricht senden Sie müssen dasselbe verwenden IMessagingComponentFactory und erstellen Sie einen Nachrichtenabsender:

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, aber bei Bedarf können Sie problemlos Ihre eigenen Schnittstellenimplementierungen erstellen IMessageSerializer и IMessageDeserializer.

Wir haben versucht, die einzigartigen Fähigkeiten jedes Warteschlangenmanagers zu bewahren, z. ViennaNET.Messaging.MQSeriesQueue ermöglicht Ihnen, nicht nur Text-, sondern auch Byte-Nachrichten zu senden ViennaNET.Messaging.RabbitMQQueue unterstützt Routing und On-the-Fly-Warteschlangen. Unser Adapter-Wrapper für RabbitMQ implementiert auch eine Art 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 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 Microservices derselben Anwendung, beispielsweise innerhalb einer Saga. Dies führte dazu, dass zusammen mit der Nachricht Zusatzdaten wie Benutzer-Login, Anforderungskennung für die End-to-End-Protokollierung, Quell-IP-Adresse und Autorisierungsdaten übermittelt werden mussten. Um die Weiterleitung dieser Daten umzusetzen, haben wir eine Bibliothek entwickelt ViennaNET.CallContext, mit dem Sie Daten aus einer an den Dienst eingehenden Anfrage speichern können. In diesem Fall spielt es keine Rolle, wie die Anfrage gestellt wurde, über eine Warteschlange oder über Http. Dann werden vor dem Senden der ausgehenden Anfrage oder Nachricht Daten aus dem Kontext entnommen und in den Headern platziert. Somit empfängt der nächste Dienst die Hilfsdaten und verwaltet sie auf die gleiche Weise.

Vielen Dank für Ihre Aufmerksamkeit, wir freuen uns auf Ihre Kommentare und Pull-Requests!

Source: habr.com

Kommentar hinzufügen