Dans cet article, nous passerons en revue les bibliothèques non encore prises en compte pour travailler avec les transactions distribuées, les files d'attente et les bases de données, qui peuvent être trouvées dans notre référentiel sur GitHub (les sources sont ici ), et les packages Nuget sont ici .
ViennaNET.Sagas
Lorsqu'un projet passe à DDD et à une architecture de microservices, alors lorsque la logique métier est répartie sur différents services, un problème se pose lié à la nécessité de mettre en œuvre le mécanisme des transactions distribuées, car de nombreux scénarios affectent souvent plusieurs domaines à la fois. Vous pouvez en savoir plus sur ces mécanismes, par exemple, dans le livre "Microservices Patterns" de Chris Richardson .
Dans nos projets, nous avons implémenté 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 d'entreprise dans lequel il est nécessaire d'effectuer séquentiellement des opérations dans différents services, tandis qu'en cas de problème à n'importe quelle étape, il est nécessaire d'appeler la procédure de restauration de toutes les étapes précédentes, où elle est fournie. Ainsi, à la fin de la saga, quel que soit le succès, nous obtenons des données cohérentes dans tous les domaines.
Notre implémentation est toujours basique et n'est pas liée à l'utilisation de méthodes d'interaction avec d'autres services. Il n'est pas difficile de l'utiliser: il suffit d'hériter 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 lors de l'exécution. La saga elle-même est une classe sans état, donc l'instance peut être placée dans la DI en tant que Singleton pour obtenir les dépendances requises.
Exemple de déclaration:
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 trouvés ici et dans l'assemblage avec tests .
ViennaNET.Orm. *
Un ensemble de bibliothèques pour travailler avec diverses bases de données via Nhibernate. Nous utilisons l'approche DB-First avec l'utilisation de Liquibase, il n'y a donc que des fonctionnalités pour travailler avec des données dans la base de données terminée.
ViennaNET.Orm.Seedwork ViennaNET.Orm
- les assemblys principaux contenant respectivement les interfaces de base et leurs implémentations. Attardons-nous sur leur contenu plus en détail.
L'interface
IEntityFactoryService
et son implémentation 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 restreindre les capacités d'une classe pour travailler avec une base de données, par exemple, pour activer des données en lecture seule. Dans de tels cas, il IEntityFactoryService
a un ancêtre - une interface IEntityRepositoryFactory
dans laquelle seule la méthode de création de référentiels est déclarée.
Pour un accès direct à la base de données, le mécanisme du fournisseur est utilisé. Pour chaque utilisé dans nos équipes de base de données ont leur propre mise en œuvre:
ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql
.
Dans le même temps, plusieurs fournisseurs peuvent être enregistrés dans une même application en même temps, ce qui permet, par exemple, dans le cadre d'un service, sans aucun coût de mise à jour de l'infrastructure, une migration pas à pas d'un SGBD à un autre. Le mécanisme de sélection de la connexion requise et, par conséquent, du fournisseur d'une classe d'entité spécifique (pour laquelle le mappage aux tables de la base de données est écrit) est mis en œuvre via l'enregistrement de 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 , requêtes et commandes directes), où l'identifiant de connexion de la configuration est pris comme argument:
"db": [
{
"nick": "mssql_connection",
"dbServerType": "MSSQL",
"ConnectionString": "...",
"useCallContext": true
},
{
"nick": "oracle_connection",
"dbServerType": "Oracle",
"ConnectionString": "..."
}
],
Exemple ApplicationContext:
internal sealed class DbContext : ApplicationContext
{
public DbContext()
{
AddEntity<SomeEntity>("mssql_connection");
AddEntity<MigratedSomeEntity>("oracle_connection");
AddEntity<AnotherEntity>("oracle_connection");
}
}
Si aucun identifiant de connexion n'est spécifié, la connexion nommée «default» sera utilisée.
Le mappage direct des entités aux 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 faciliter l'écriture de référentiels stub dans les tests unitaires, il existe une bibliothèque
ViennaNET.TestUtils.Orm
.
Des exemples complets d'utilisation de ViennaNET.Orm. * Vous pouvez trouver ici .
ViennaNET.Messaging. *
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 qu'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é. La bibliothèque
ViennaNET.Messaging
est uniquement responsable de cette unification et ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue ViennaNET.Messaging.KafkaQueue
contient les implémentations d'adaptateur pour IBM MQ, RabbitMQ et Kafka, respectivement.
Il existe deux processus dans l'utilisation des files d'attente: la réception d'un message et l'envoi.
Pensez à obtenir. Il y a 2 options ici: pour une écoute constante et pour recevoir un seul message. Pour écouter constamment 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. De plus, il doit être "lié" à une file d'attente spécifique, cela se fait en s'enregistrant IQueueReactorFactory
en spécifiant l'identifiant de file d'attente de la configuration:
"messaging": {
"ApplicationName": "MyApplication"
},
"rabbitmq": {
"queues": [
{
"id": "myQueue",
"queuename": "lalala",
...
}
]
},
Un exemple de démarrage 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 à écouter, tous les messages de la file d'attente spécifiée iront au processeur correspondant.
Pour recevoir un seul message dans l'interface d'usine,
IMessagingComponentFactory
il existe une méthode CreateMessageReceiver
qui créera un destinataire en attente d'un message de la file d'attente spécifiée:
using (var receiver = _messagingComponentFactory.CreateMessageReceiver<TestMessage>("myQueue"))
{
var message = receiver.Receive();
}
Pour envoyer un message, vous devez utiliser le même
IMessagingComponentFactory
et créer 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: juste du texte, XML et JSON, mais si nécessaire, vous pouvez facilement créer vos propres implémentations des interfaces
IMessageSerializer IMessageDeserializer
.
Nous avons essayé de préserver les capacités uniques de chaque gestionnaire de files d'attente, par exemple,
ViennaNET.Messaging.MQSeriesQueue
il permet d'envoyer non seulement des messages texte, mais aussi 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 créée pour un seul message de réponse.
Voici 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 application, par exemple, dans le cadre d'une saga. Cela a conduit à la nécessité de transférer avec le message des données auxiliaires telles que le nom d'utilisateur, l'ID de demande pour la journalisation de bout en bout, l'adresse IP source et les données d'autorisation. Pour implémenter la transmission de ces données, une bibliothèque a été développée
ViennaNET.CallContext
qui 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 la file d'attente ou via Http, n'a pas d'importance. Ensuite, avant d'envoyer une demande ou un message sortant, les données sont extraites du contexte et placées dans les en-têtes. Ainsi, le service suivant reçoit des données auxiliaires et les dispose de la même manière.
Merci pour votre attention, nous attendons vos commentaires et pull requests!