ViennaNET : un ensemble de bibliothèques pour le backend. Partie 2

La communauté des développeurs .NET de Raiffeisenbank continue d'examiner brièvement le contenu de ViennaNET. Sur comment et pourquoi nous en sommes arrivés là, tu peux lire la première partie.

Dans cet article, nous passerons en revue les bibliothèques encore à considérer pour travailler avec des transactions distribuées, des files d'attente et des bases de données, qui peuvent être trouvées dans notre référentiel GitHub (les sources sont ici), et Forfaits Nuget ici.

ViennaNET : un ensemble de bibliothèques pour le backend. Partie 2

ViennaNET.Sagas

Lorsqu'un projet passe à l'architecture DDD et microservices, lorsque la logique métier est distribuée entre différents services, un problème se pose lié à la nécessité de mettre en œuvre un mécanisme de transaction distribué, car de nombreux scénarios affectent souvent plusieurs domaines à la fois. Vous pouvez vous familiariser plus en détail avec ces mécanismes, par exemple : dans le livre "Microservices Patterns", Chris Richardson.

Dans nos projets, nous avons mis en place un mécanisme simple mais utile : une saga, ou plutôt une saga basée sur l'orchestration. Son essence est la suivante : il existe un certain scénario commercial dans lequel il est nécessaire d'effectuer séquentiellement des opérations dans différents services, et si des problèmes surviennent à une étape quelconque, il est nécessaire d'appeler la procédure de restauration pour toutes les étapes précédentes, où elle est fourni. Ainsi, à la fin de la saga, quel que soit le succès, nous recevons des données cohérentes dans tous les domaines.

Notre implémentation est toujours réalisée sous sa forme de base et n'est liée à l'utilisation d'aucune méthode d'interaction avec d'autres services. Ce n'est pas difficile à utiliser : créez simplement un descendant de la classe abstraite de base SagaBase<T>, où T est votre classe de contexte dans laquelle vous pouvez stocker les données initiales nécessaires au fonctionnement de la saga, ainsi que quelques résultats intermédiaires. L'instance de contexte sera transmise à toutes les étapes pendant l'exécution. Saga elle-même est une classe sans état, l'instance peut donc être placée dans DI en tant que Singleton pour obtenir les dépendances nécessaires.

Exemple d'annonce :

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

Exemple d'appel :

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

Des exemples complets de différentes implémentations peuvent être consultés ici et en assemblée avec essais.

VienneNET.Orm.*

Un ensemble de bibliothèques pour travailler avec diverses bases de données via Nhibernate. Nous utilisons l'approche DB-First en utilisant Liquibase, il n'existe donc que des fonctionnalités permettant de travailler avec des données dans une base de données prête à l'emploi.

ViennaNET.Orm.Seedwork и ViennaNET.Orm – les assemblys principaux contenant respectivement les interfaces de base et leurs implémentations. Examinons leur contenu plus en détail.

Interface IEntityFactoryService et sa mise en œuvre EntityFactoryService sont le principal point de départ pour travailler avec la base de données, puisque l'unité de travail, des référentiels pour travailler avec des entités spécifiques, ainsi que des exécuteurs de commandes et des requêtes SQL directes sont créés ici. Parfois, il est pratique de limiter les capacités d'une classe à travailler avec une base de données, par exemple, pour permettre de lire uniquement des données. Pour de tels cas IEntityFactoryService il y a un ancêtre - interface IEntityRepositoryFactory, qui déclare uniquement une méthode de création de référentiels.

Pour accéder directement à la base de données, le mécanisme du fournisseur est utilisé. Chaque SGBD que nous utilisons dans nos équipes a sa propre implémentation : ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Parallèlement, plusieurs prestataires peuvent être enregistrés simultanément dans une même application, ce qui permet, par exemple, dans le cadre d'un même service, sans aucun frais de modification de l'infrastructure, d'effectuer une migration étape par étape depuis d'un SGBD à un autre. Le mécanisme de sélection de la connexion requise et, par conséquent, du fournisseur pour une classe d'entité spécifique (pour laquelle le mappage vers les tables de base de données est écrit) est implémenté en enregistrant l'entité dans la classe BoundedContext (contient une méthode d'enregistrement des entités de domaine) ou son successeur. ApplicationContext (contient des méthodes d'enregistrement des entités d'application, des requêtes directes et des commandes), où l'identifiant de connexion de la configuration est accepté comme argument :

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

Exemple de contexte d'application :

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

Si l’ID de connexion n’est pas spécifié, alors la connexion nommée « par défaut » sera utilisée.

Le mappage direct des entités vers les tables de base de données est implémenté à l'aide des outils NHibernate standard. Vous pouvez utiliser la description à la fois via des fichiers XML et via des classes. Pour une écriture pratique des référentiels de stub dans les tests unitaires, il existe une bibliothèque ViennaNET.TestUtils.Orm.

Des exemples complets d'utilisation de ViennaNET.Orm.* peuvent être trouvés ici.

ViennaNET.Messagerie.*

Un ensemble de bibliothèques pour travailler avec les files d'attente.

Pour travailler avec les files d'attente, la même approche a été choisie comme avec différents SGBD, à savoir l'approche unifiée maximale possible en termes de travail avec la bibliothèque, quel que soit le gestionnaire de files d'attente utilisé. Bibliothèque ViennaNET.Messaging est précisément responsable de cette unification, et ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue и ViennaNET.Messaging.KafkaQueue contiennent des implémentations d'adaptateur pour IBM MQ, RabbitMQ et Kafka, respectivement.

Lorsque vous travaillez avec des files d'attente, il existe deux processus : recevoir un message et l'envoyer.

Pensez à recevoir. Il y a ici 2 options : pour une écoute continue et pour recevoir un seul message. Pour écouter en permanence la file d'attente, vous devez d'abord décrire la classe de processeur héritée de IMessageProcessor, qui sera responsable du traitement du message entrant. Ensuite, il doit être « lié » à une file d'attente spécifique ; cela se fait par l'enregistrement dans IQueueReactorFactory indiquant l'identifiant de la file d'attente issu de la configuration :

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

Exemple de début d'écoute :

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

Ensuite, lorsque le service démarre et que la méthode est appelée pour commencer l'écoute, tous les messages de la file d'attente spécifiée seront envoyés au processeur correspondant.

Pour recevoir un seul message dans une interface d'usine IMessagingComponentFactory il y a une méthode CreateMessageReceiverqui créera un destinataire en attente d'un message de la file d'attente qui lui est spécifiée :

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

Pour envoyer un message tu dois utiliser le même IMessagingComponentFactory et créez un expéditeur de message :

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

Il existe trois options prêtes à l'emploi pour sérialiser et désérialiser un message : uniquement du texte, XML et JSON, mais si nécessaire, vous pouvez facilement créer vos propres implémentations d'interface. IMessageSerializer и IMessageDeserializer.

Nous avons essayé de préserver les capacités uniques de chaque gestionnaire de files d'attente, par ex. ViennaNET.Messaging.MQSeriesQueue vous permet d'envoyer non seulement du texte, mais également des messages d'octets, et ViennaNET.Messaging.RabbitMQQueue prend en charge le routage et la mise en file d'attente à la volée. Notre wrapper d'adaptateur pour RabbitMQ implémente également un semblant de RPC : nous envoyons un message et attendons une réponse d'une file d'attente temporaire spéciale, qui n'est créée que pour un message de réponse.

Ici un exemple d'utilisation de files d'attente avec des nuances de connexion de base.

ViennaNET.CallContext

Nous utilisons les files d'attente non seulement pour l'intégration entre différents systèmes, mais aussi pour la communication entre les microservices d'une même application, par exemple au sein d'une saga. Cela a conduit à la nécessité de transmettre avec le message des données auxiliaires telles que la connexion de l'utilisateur, l'identifiant de la demande pour la journalisation de bout en bout, l'adresse IP source et les données d'autorisation. Pour mettre en œuvre la transmission de ces données, nous avons développé une bibliothèque ViennaNET.CallContext, qui vous permet de stocker les données d'une requête entrant dans le service. Dans ce cas, la manière dont la demande a été effectuée, via une file d'attente ou via Http, n'a pas d'importance. Ensuite, avant d'envoyer la requête ou le message sortant, les données sont extraites du contexte et placées dans les en-têtes. Ainsi, le service suivant reçoit les données auxiliaires et les gère de la même manière.

Merci de votre attention, nous attendons avec impatience vos commentaires et pull request !

Source: habr.com

Ajouter un commentaire