A comunidade de desenvolvedores .NET do Raiffeisenbank continua a breve análise do conteúdo do ViennaNET. Sobre como e por que chegamos a isso, .
Neste artigo, abordaremos bibliotecas ainda não discutidas para trabalhar com transações distribuídas, filas e bancos de dados, que podem ser encontradas em nosso repositório no GitHub (), e .
ViennaNET.Sagas
Quando um projeto faz a transição para DDD e arquitetura de microserviços, surge o problema da necessidade de implementar um mecanismo de transações distribuídas, pois muitos cenários muitas vezes envolvem vários domínios. Mecanismos como esses podem ser melhor compreendidos, por exemplo, .
Em nossos projetos, implementamos um mecanismo simples, mas útil: a saga, ou mais precisamente, uma saga baseada em orquestração. A essência dela é a seguinte: existe um determinado cenário de negócios que requer a execução sequencial de operações em diferentes serviços, e, caso ocorra algum problema em qualquer etapa, é necessário acionar um procedimento para reverter todas as etapas anteriores, onde for previsto. Assim, ao final da execução da saga, independentemente do sucesso, obtemos dados consistentes em todos os domínios.
Nossa implementação ainda está em uma forma básica e não está ligada ao uso de qualquer método de interação com outros serviços. Sua aplicação não é difícil: basta fazer uma classe filha da classe base abstrata SagaBase, onde T é sua classe de contexto, na qual podem ser armazenados os dados necessários para o trabalho da saga, bem como alguns resultados intermediários. Uma instância do contexto será passada para todas as etapas durante a execução. A saga em si é uma classe stateless, portanto, a instância pode ser registrada no DI como Singleton, para obter as dependências necessárias.
Exemplo de declaração:
public class ExampleSaga : SagaBase
{
public ExampleSaga()
{
Step("Passo 1")
.WithAction(c => ...)
.WithCompensation(c => ...);
AsyncStep("Passo 2")
.WithAction(async c => ...);
}
}
Exemplo de chamada:
var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);
Exemplos completos de diferentes implementações podem ser vistos e na coleção com .
ViennaNET.Orm.*
Conjunto de bibliotecas para trabalhar com diferentes bancos de dados através do Nhibernate. Utilizamos a abordagem DB-First com a aplicação do Liquibase, portanto, aqui está apenas a funcionalidade para trabalhar com dados em um banco de dados pronto.
ViennaNET.Orm.Seedwork e ViennaNET.Orm – os principais assemblies que contêm as interfaces básicas e suas implementações, respectivamente. Vamos detalhar seu conteúdo.
Interface IEntityFactoryService e sua implementação EntityFactoryService são o principal ponto de partida para trabalhar com o banco de dados, pois aqui se cria o Unit of Work, repositórios para trabalhar com entidades específicas, bem como executores de comandos e consultas SQL diretas. Às vezes, é conveniente limitar as capacidades da classe para trabalhar com o banco de dados, por exemplo, permitindo apenas a leitura de dados. Para tais casos, há IEntityFactoryService um antecessor – a interface IEntityRepositoryFactory, onde somente o método para criação de repositórios é declarado.
Para acessos diretos ao banco de dados, utiliza-se o mecanismo de provedores. Para cada sistema de gerenciamento de banco de dados (SGBD) usado em nossas equipes, há sua própria implementação: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.
Assim, em uma única aplicação, podem ser registrados vários provedores ao mesmo tempo, o que permite, por exemplo, realizar uma migração gradual de um SGBD para outro dentro de um único serviço, sem custos adicionais para modificar a infraestrutura. O mecanismo para escolher a conexão necessária e, portanto, o provedor para uma classe entidade específica (para a qual a mapeação para as tabelas do banco de dados é escrita) é implementado através do registro da entidade na classe BoundedContext (que contém um método para registrar entidades de domínio) ou seu herdeiro ApplicationContext (que contém métodos para registrar entidades de aplicação, consultas diretas e comandos), onde como argumento é aceito o identificador da conexão da configuração:
"db": [
{
"nick": "mssql_connection",
"dbServerType": "MSSQL",
"ConnectionString": "...",
"useCallContext": true
},
{
"nick": "oracle_connection",
"dbServerType": "Oracle",
"ConnectionString": "..."
}
],
Exemplo de ApplicationContext:
internal sealed class DbContext : ApplicationContext
{
public DbContext()
{
AddEntity("mssql_connection");
AddEntity("oracle_connection");
AddEntity("oracle_connection");
}
}
Se o identificador de conexão não for especificado, será utilizada a conexão com o nome "default".
O mapeamento de entidades para tabelas de banco de dados é realizado utilizando as ferramentas padrão do NHibernate. É possível usar a descrição tanto por meio de arquivos XML quanto por classes. Para facilitar a escrita de repositórios simulados em testes de unidade, existe uma biblioteca ViennaNET.TestUtils.Orm.
Exemplos completos de uso do ViennaNET.Orm.* podem ser encontrados .
ViennaNET.Messaging.*
Um conjunto de bibliotecas para trabalhar com filas.
Para trabalhar com filas, foi escolhida a mesma abordagem utilizada com diversos SGBDs, ou seja, uma abordagem unificada na medida do possível em termos de interação com a biblioteca, independentemente do gerenciador de fila utilizado. A biblioteca ViennaNET.Messaging é responsável por essa unificação, e ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue e ViennaNET.Messaging.KafkaQueue contêm implementações de adaptadores para IBM MQ, RabbitMQ e Kafka, respectivamente.
No trabalho com filas, existem dois processos: recebimento de mensagens e envio.
Vamos considerar o recebimento. Aqui existem 2 opções: para escuta contínua e para recebimento de uma única mensagem. Para escutar a fila continuamente, é necessário primeiro descrever uma classe de processador, herdada de IMessageProcessor, que será responsável pelo processamento da mensagem recebida. Em seguida, é necessário "vinculá-la" a uma fila específica, o que é feito por meio do registro em IQueueReactorFactory com a especificação do identificador da fila a partir da configuração:
"messaging": {
"ApplicationName": "MyApplication"
},
"rabbitmq": {
"queues": [
{
"id": "myQueue",
"queuename": "lalala",
...
}
]
},
Exemplo de inicialização da escuta:
_queueReactorFactory.Register<MyMessageProcessor>("myQueue");
var queueReactor = queueReactorFactory.CreateQueueReactor("myQueue");
queueReactor.StartProcessing();
Assim, ao iniciar o serviço e chamar o método para começar a escuta, todas as mensagens da fila especificada chegarão ao processador correspondente.
Para receber uma única mensagem na interface-fábrica IMessagingComponentFactory existe o método CreateMessageReceiver, que criará um receptor aguardando mensagens da fila especificada:
using (var receiver = _messagingComponentFactory.CreateMessageReceiver<TestMessage>("myQueue"))
{
var message = receiver.Receive();
}
Para enviar uma mensagem é necessário utilizar a mesma IMessagingComponentFactory e criar um remetente de mensagem:
using (var sender = _messagingComponentFactory.CreateMessageSender<MyMessage>("myQueue"))
{
sender.SendMessage(new MyMessage { Value = ...});
}
Para a serialização e desserialização de mensagens, existem três opções prontas: texto simples, XML e JSON, mas se necessário, podemos facilmente fazer nossas próprias implementações de interfaces. IMessageSerializer e IMessageDeserializer.
Tentamos manter as capacidades exclusivas de cada gerenciador de filas, por exemplo, ViennaNET.Messaging.MQSeriesQueue permite enviar não apenas mensagens de texto, mas também mensagens em bytes, e ViennaNET.Messaging.RabbitMQQueue oferece suporte ao roteamento e à criação de filas “em tempo real”. Em nossa camada de adaptador para RabbitMQ, também implementamos algo semelhante a RPC: enviamos uma mensagem e aguardamos uma resposta de uma fila temporária especial, que é criada apenas para uma única mensagem de resposta.
Aqui .
ViennaNET.CallContext
Usamos filas não apenas para a integração entre diferentes sistemas, mas também para a comunicação entre microsserviços de uma única aplicação, por exemplo, dentro de uma saga. Isso levou à necessidade de transmitir junto com a mensagem dados auxiliares, como o login do usuário, o identificador da solicitação para log de rastreamento, o endereço IP da origem e dados de autorização. Para implementar o repasse desses dados, desenvolvemos uma biblioteca ViennaNET.CallContext, que permite armazenar dados da solicitação de entrada no serviço. O método de realização da solicitação, seja por meio de uma fila ou via Http, não importa. Em seguida, antes de enviar a solicitação ou mensagem de saída, os dados são recuperados do contexto e colocados nos cabeçalhos. Assim, o próximo serviço recebe os dados auxiliares e os utiliza da mesma forma.
Obrigado pela atenção, aguardamos seus comentários e pull requests!
Fonte: habr.com
