ViennaNET: um conjunto de bibliotecas para backend. Parte 2

A comunidade de desenvolvimento do Raiffeisenbank .NET continua com uma breve análise do ViennaNET. Você pode ler sobre como e por que chegamos a isso na primeira parte .



Neste artigo, iremos percorrer as bibliotecas ainda não consideradas para trabalhar com transações distribuídas, filas e bancos de dados, que podem ser encontradas em nosso repositório no GitHub (a fonte está aqui ), e os pacotes Nuget estão aqui .







ViennaNET.Sagas



Quando um projeto faz a transição para DDD e uma arquitetura de microsserviço, então, quando a lógica de negócios é espalhada por diferentes serviços, surge um problema associado à necessidade de implementar o mecanismo de transações distribuídas, porque muitos cenários geralmente afetam vários domínios ao mesmo tempo. Você pode aprender mais sobre esses mecanismos, por exemplo, no livro "Padrões de Microserviços" de Chris Richardson .



Em nossos projetos, implementamos um mecanismo simples, mas útil: uma saga, ou melhor, uma saga baseada na orquestração. Sua essência é a seguinte: existe um determinado cenário de negócios em que é necessário realizar operações sequencialmente em diferentes serviços, enquanto, em caso de problemas em qualquer etapa, é necessário chamar o procedimento de rollback de todas as etapas anteriores, onde estiver previsto. Assim, no final da saga, independentemente do sucesso, obtemos dados consistentes em todos os domínios.



Nossa implementação ainda está em sua forma básica e não está vinculada ao uso de quaisquer métodos de interação com outros serviços. Não é difícil usá-lo: basta herdar da classe abstrata base SagaBase <T>, onde T é a sua classe de contexto, na qual você pode armazenar os dados iniciais necessários para que a saga funcione, bem como alguns resultados intermediários. A instância do contexto será encaminhada para todas as etapas em tempo de execução. A própria saga é uma classe sem estado, portanto, a instância pode ser colocada no DI como um Singleton para obter as dependências necessárias.



Declaração de exemplo:



public class ExampleSaga : SagaBase<ExampleContext>
{
  public ExampleSaga()
  {
    Step("Step 1")
      .WithAction(c => ...)
      .WithCompensation(c => ...);
	
    AsyncStep("Step 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 encontrados aqui e na montagem com testes .



ViennaNET.Orm. *



Um conjunto de bibliotecas para trabalhar com vários bancos de dados por meio do Nhibernate. Usamos a abordagem DB-First com o uso do Liquibase, portanto, há apenas funcionalidade para trabalhar com dados no banco de dados concluído.



ViennaNET.Orm.Seedwork ViennaNET.Orm- assemblies principais contendo interfaces básicas e suas implementações, respectivamente. Vamos nos deter em seu conteúdo com mais detalhes.



A interface IEntityFactoryServicee sua implementação EntityFactoryServicesão o principal ponto de partida para trabalhar com o banco de dados, já que aqui são criados a Unidade de Trabalho, repositórios para trabalhar com entidades específicas, bem como executores de comandos e consultas SQL diretas. Às vezes, é conveniente restringir os recursos de uma classe para trabalhar com um banco de dados, por exemplo, para habilitar dados somente leitura. Para tais casos, ele IEntityFactoryServicepossui um ancestral - uma interface IEntityRepositoryFactoryna qual apenas o método de criação de repositórios é declarado.



Para acesso direto ao banco de dados, o mecanismo do provedor é usado. Para cada usado em nossas equipes de base de dados tem sua própria implementação: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.



Ao mesmo tempo, vários provedores podem ser cadastrados em uma aplicação ao mesmo tempo, o que permite, por exemplo, no âmbito de um serviço, sem nenhum custo de atualização da infraestrutura, uma migração passo a passo de um SGBD para outro. O mecanismo para selecionar a conexão necessária e, portanto, o provedor para uma classe de entidade específica (para a qual o mapeamento para as tabelas do banco de dados é escrito) é implementado por meio do registro da entidade na classe BoundedContext (contém um método para registrar entidades de domínio) ou seu sucessor ApplicationContext (contém métodos para registrar entidades de aplicativo , solicitações diretas e comandos), em que o identificador de conexão da configuração é considerado um argumento:



"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<SomeEntity>("mssql_connection");
    AddEntity<MigratedSomeEntity>("oracle_connection");
    AddEntity<AnotherEntity>("oracle_connection");
  }
}


Se nenhum identificador de conexão for especificado, a conexão chamada "default" será usada.



O mapeamento direto de entidades para tabelas de banco de dados é implementado usando ferramentas padrão do NHibernate. Você pode usar a descrição por meio de arquivos xml e por meio de classes. Para escrever repositórios stub convenientes em testes de unidade, existe uma biblioteca ViennaNET.TestUtils.Orm.



Exemplos completos de como usar ViennaNET.Orm. * Podem ser encontrados aqui .



ViennaNET.Messaging. *



Um conjunto de bibliotecas para trabalhar com filas.



Para trabalhar com filas, foi escolhida a mesma abordagem que com vários SGBDs, ou seja, a abordagem unificada máxima possível em termos de trabalho com a biblioteca, independentemente do gerenciador de filas usado. A biblioteca ViennaNET.Messagingé apenas responsável por esta unificação e ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue ViennaNET.Messaging.KafkaQueuecontém as implementações do adaptador para IBM MQ, RabbitMQ e Kafka, respectivamente.



Existem dois processos para trabalhar com filas: receber uma mensagem e enviar.



Considere comprar. Existem 2 opções aqui: para ouvir constantemente e para receber uma única mensagem. Para ouvir constantemente a fila, primeiro você precisa descrever a classe de processador herdada deIMessageProcessor, que será responsável pelo processamento da mensagem recebida. Além disso, ele deve ser "vinculado" a uma fila específica, isso é feito registrando IQueueReactorFactory-se especificando o identificador de fila da configuração:



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


Um exemplo de como começar a ouvir:



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


Então, quando o serviço é iniciado e o método é chamado para começar a escutar, todas as mensagens da fila especificada irão para o processador correspondente.



Para receber uma única mensagem na interface de fábrica, IMessagingComponentFactoryexiste um método CreateMessageReceiverque criará um destinatário esperando por uma mensagem da fila especificada:



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


Para enviar uma mensagem, você deve usar o mesmo IMessagingComponentFactorye criar um remetente da mensagem:



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


Existem três opções prontas para serializar e desserializar uma mensagem: apenas texto, XML e JSON, mas se necessário, você pode fazer com segurança suas próprias implementações das interfaces IMessageSerializer IMessageDeserializer.



Tentamos preservar os recursos exclusivos de cada gerenciador de filas, por exemplo, ViennaNET.Messaging.MQSeriesQueueele permite o envio não apenas de mensagens de texto, mas também de mensagens de byte e ViennaNET.Messaging.RabbitMQQueueoferece suporte ao roteamento e à criação de filas em tempo real. Nosso wrapper de adaptador para RabbitMQ também implementa alguma aparência de RPC: enviamos uma mensagem e esperamos por uma resposta de uma fila temporária especial que é criada para apenas uma mensagem de resposta.



Aqui está um exemplo de uso de filas com nuances básicas de conexão .



ViennaNET.CallContext



Usamos filas não apenas para integração entre diferentes sistemas, mas também para comunicação entre microsserviços de um aplicativo, por exemplo, no âmbito de uma saga. Isso levou à necessidade de transferir junto com a mensagem dados auxiliares como nome de usuário, ID de solicitação para registro de ponta a ponta, endereço IP de origem e dados de autorização. Para implementar o encaminhamento desses dados, foi desenvolvida uma biblioteca ViennaNET.CallContextque permite armazenar os dados de uma solicitação de entrada no serviço. Nesse caso, não importa como a solicitação foi feita, por meio da fila ou por meio do Http. Então, antes de enviar uma solicitação ou mensagem de saída, os dados são retirados do contexto e colocados nos cabeçalhos. Assim, o próximo serviço recebe dados auxiliares e os dispõe da mesma forma.



Obrigado por sua atenção, estamos ansiosos para seus comentários e solicitações!



All Articles