La comunidad de desarrolladores .NET de Raiffeisenbank continúa con un breve análisis del contenido de ViennaNET. Sobre cómo y por qué llegamos a esto, .
En este artículo, revisaremos bibliotecas aún no tratadas para trabajar con transacciones distribuidas, colas y bases de datos, que se pueden encontrar en nuestro repositorio en GitHub (), y .
ViennaNET.Sagas
Cuando un proyecto hace la transición a DDD y arquitectura de microservicios, surge un problema relacionado con la necesidad de implementar un mecanismo de transacciones distribuidas, ya que muchos escenarios a menudo abarcan varios dominios a la vez. Se puede conocer más sobre tales mecanismos, por ejemplo, .
En nuestros proyectos, hemos implementado un mecanismo simple pero útil: una saga, o más precisamente, una saga basada en orquestación. Su esencia radica en lo siguiente: hay un cierto escenario de negocio en el que es necesario realizar operaciones secuencialmente en diferentes servicios, y si se produce algún problema en cualquiera de los pasos, es necesario invocar un procedimiento de retroceso de todos los pasos anteriores donde esté previsto. Así, al final de la ejecución de la saga, independientemente del éxito, obtenemos datos consistentes en todos los dominios.
Nuestra implementación está realizada hasta ahora de manera básica y no está atada al uso de ningún método de interacción con otros servicios. Su aplicación no es complicada: basta con hacer un heredero de la clase abstracta básica SagaBase, donde T es tu clase de contexto, en la que se pueden almacenar los datos originales necesarios para trabajar con la saga, así como algunos resultados intermedios. La instancia del contexto se pasará a todos los pasos durante la ejecución. La propia saga es una clase sin estado, por lo que la instancia puede ser registrada en DI como Singleton para obtener las dependencias necesarias.
Ejemplo de declaración:
public class ExampleSaga : SagaBase
{
public ExampleSaga()
{
Step("Paso 1")
.WithAction(c => ...)
.WithCompensation(c => ...);
AsyncStep("Paso 2")
.WithAction(async c => ...);
}
}
Ejemplo de llamada:
var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);
Se pueden ver ejemplos completos de diferentes implementaciones y en el conjunto con .
ViennaNET.Orm.*
Conjunto de bibliotecas para trabajar con diversas bases de datos a través de Nhibernate. Utilizamos un enfoque DB-First con la implementación de Liquibase, por lo que aquí solo se incluye la funcionalidad para trabajar con datos en una base de datos existente.
ViennaNET.Orm.Seedwork y ViennaNET.Orm son los ensamblados principales que contienen las interfaces base y sus implementaciones, respectivamente. Vamos a detenernos más en su contenido.
Interfaz IEntityFactoryService y su implementación EntityFactoryService son el punto de partida principal para trabajar con la base de datos, ya que aquí se crea la Unidad de Trabajo, repositorios para trabajar con entidades específicas, así como ejecutores de comandos y consultas SQL directas. A veces es conveniente restringir las capacidades de la clase para trabajar con la base de datos, por ejemplo, permitir solo la lectura de datos. Para tales casos, IEntityFactoryService hay un ancestro: la interfaz IEntityRepositoryFactory, en la que se declara únicamente el método para crear repositorios.
Para el acceso directo a la base de datos se utiliza el mecanismo de proveedores. Para cada sistema de gestión de bases de datos que utilizamos en nuestros equipos, hay su propia implementación: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.
En una aplicación puede registrarse simultáneamente varios proveedores, lo que permite, por ejemplo, realizar una migración paso a paso de un sistema de gestión de bases de datos a otro dentro de un mismo servicio sin ningún costo en la mejora de la infraestructura. El mecanismo para elegir la conexión necesaria y, por lo tanto, el proveedor para una clase de entidad específica (para la que se escribe el mapeo en tablas de la base de datos) se implementa a través del registro de la entidad en la clase BoundedContext (que contiene el método para registrar entidades de dominio) o su heredero ApplicationContext (que contiene métodos para registrar entidades de aplicación, consultas directas y comandos), donde como argumento se toma el identificador de conexión de la configuración:
"db": [
{
"nick": "mssql_connection",
"dbServerType": "MSSQL",
"ConnectionString": "...",
"useCallContext": true
},
{
"nick": "oracle_connection",
"dbServerType": "Oracle",
"ConnectionString": "..."
}
],
Ejemplo de ApplicationContext:
internal sealed class DbContext : ApplicationContext
{
public DbContext()
{
AddEntity<SomeEntity>("mssql_connection");
AddEntity<MigratedSomeEntity>("oracle_connection");
AddEntity<AnotherEntity>("oracle_connection");
}
}
Si no se especifica el identificador de conexión, se utilizará la conexión con el nombre "default".
La mapeo de entidades a tablas de base de datos se implementa mediante las herramientas estándar de NHibernate. Se puede utilizar la descripción tanto a través de archivos xml como mediante clases. Para facilitar la escritura de repositorios simulados en pruebas unitarias, hay una biblioteca ViennaNET.TestUtils.Orm.
Se pueden encontrar ejemplos completos de uso de ViennaNET.Orm.* .
ViennaNET.Messaging.*
Conjunto de bibliotecas para trabajar con colas.
Para trabajar con colas se eligió el mismo enfoque que con diversas bases de datos, es decir, un enfoque lo más unificado posible desde el punto de vista del trabajo con la biblioteca, independientemente del gestor de colas utilizado. La biblioteca ViennaNET.Messaging se encarga precisamente de esta unificación, y ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue y ViennaNET.Messaging.KafkaQueue contienen implementaciones de adaptadores para IBM MQ, RabbitMQ y Kafka respectivamente.
En el trabajo con colas hay dos procesos: recibir un mensaje y enviarlo.
Consideremos la recepción. Aquí hay 2 opciones: para la escucha continua y para recibir un único mensaje. Para la escucha continua de la cola es necesario primero describir una clase procesadora que herede de IMessageProcessor, que se encargará del procesamiento del mensaje entrante. Luego, debe 'vincularse' a una cola determinada, lo que se hace a través del registro en IQueueReactorFactory especificando el identificador de la cola de la configuración:
"messaging": {
"ApplicationName": "MyApplication"
},
"rabbitmq": {
"queues": [
{
"id": "myQueue",
"queuename": "lalala",
...
}
]
},
Ejemplo de inicio de la escucha:
_queueReactorFactory.Register<MyMessageProcessor>("myQueue");
var queueReactor = queueReactorFactory.CreateQueueReactor("myQueue");
queueReactor.StartProcessing();
Luego, al iniciar el servicio y llamar al método para comenzar la escucha, todos los mensajes de la cola especificada serán dirigidos al procesador correspondiente.
Para recibir un único mensaje en la interfaz de fábrica IMessagingComponentFactory hay un método CreateMessageReceiver, que creará un receptor que estará esperando mensajes de la cola que se le haya indicado:
using (var receiver = _messagingComponentFactory.CreateMessageReceiver<TestMessage>("myQueue"))
{
var message = receiver.Receive();
}
Para enviar un mensaje es necesario utilizar la misma IMessagingComponentFactory y crear un emisor de mensajes:
using (var sender = _messagingComponentFactory.CreateMessageSender<MyMessage>("myQueue"))
{
sender.SendMessage(new MyMessage { Value = ...});
}
Para la serialización y deserialización de mensajes, hay tres opciones listas: texto simple, XML y JSON, pero si es necesario, se pueden crear implementaciones personalizadas de las interfaces. IMessageSerializer e IMessageDeserializer.
Hemos tratado de conservar las capacidades únicas de cada gestor de colas, por ejemplo, ViennaNET.Messaging.MQSeriesQueue permite enviar no solo mensajes de texto, sino también mensajes en bytes, y ViennaNET.Messaging.RabbitMQQueue admite el enrutamiento y la creación de colas "sobre la marcha". En nuestro envoltorio adaptador para RabbitMQ, también se ha implementado algo parecido a RPC: enviamos un mensaje y esperamos una respuesta de una cola temporal especial, que se crea solo para un mensaje de respuesta.
Aquí .
ViennaNET.CallContext
Utilizamos las colas no solo para la integración entre diferentes sistemas, sino también para la comunicación entre microservicios de una misma aplicación, por ejemplo, en el marco de una saga. Esto ha llevado a la necesidad de enviar, junto con el mensaje, datos auxiliares como el nombre de usuario, el identificador de solicitud para el registro completo, la dirección IP de origen y las credenciales de autorización. Para implementar el paso de estos datos, hemos desarrollado una biblioteca ViennaNET.CallContext, que permite almacenar datos de la solicitud entrante al servicio. De esta manera, la forma en que se hizo la solicitud, ya sea a través de una cola o mediante Http, no tiene importancia. Luego, antes de enviar la solicitud o mensaje de salida, se extraen los datos del contexto y se colocan en las cabeceras. De este modo, el siguiente servicio recibe los datos auxiliares y los gestiona de manera similar.
¡Gracias por su atención, esperamos sus comentarios y pull requests!
Fuente: habr.com
