ViennaNET: um conjunto de bibliotecas para backend. Parte 2

A comunidade de desenvolvedores do Raiffeisenbank .NET continua revisando brevemente o conteúdo do ViennaNET. Sobre como e por que chegamos a isso, você pode ler a primeira parte.

Neste artigo, examinaremos bibliotecas ainda a serem consideradas para trabalhar com transações distribuídas, filas e bancos de dados, que podem ser encontradas em nosso repositório GitHub (as fontes estão aqui) e Pacotes Nuget aqui.

ViennaNET: um conjunto de bibliotecas para backend. Parte 2

VienaNET.Sagas

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

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

Nossa implementação ainda é feita em sua forma básica e não está vinculada à utilização de quaisquer métodos de interação com outros serviços. Não é difícil de usar: basta fazer um descendente da classe abstrata base SagaBase<T>, onde T é a sua classe de contexto na qual você pode armazenar os dados iniciais necessários para o funcionamento da saga, bem como alguns resultados intermediários. A instância de contexto será passada para todas as etapas durante a execução. O próprio Saga é uma classe sem estado, portanto a instância pode ser colocada em DI como um Singleton para obter as dependências necessárias.

Exemplo de anúncio:

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

Chamada de exemplo:

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

Exemplos completos de diferentes implementações podem ser visualizados aqui e em montagem com testes.

ViennaNET.Orm.*

Um conjunto de bibliotecas para trabalhar com diversos bancos de dados via Nhibernate. Usamos a abordagem DB-First usando Liquibase, portanto, há apenas funcionalidade para trabalhar com dados em um banco de dados pronto.

ViennaNET.Orm.Seedwork и ViennaNET.Orm – principais assemblies contendo interfaces básicas e suas implementações, respectivamente. Vejamos seu conteúdo com mais detalhes.

Interface. IEntityFactoryService e sua implementação EntityFactoryService sã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 limitar os recursos de uma classe para trabalhar com um banco de dados, por exemplo, para fornecer a capacidade de apenas ler dados. Para tais casos IEntityFactoryService existe um ancestral - interface IEntityRepositoryFactory, que declara apenas um método para criar repositórios.

Para acessar diretamente o banco de dados, é utilizado o mecanismo de provedor. Cada SGBD que utilizamos em nossas equipes possui sua própria implementação: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Ao mesmo tempo, vários prestadores podem ser registados numa aplicação ao mesmo tempo, o que permite, por exemplo, no âmbito de um serviço, sem quaisquer custos de modificação da infra-estrutura, realizar 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 tabelas de banco de dados é escrito) é implementado através 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 aplicação, solicitações diretas e comandos), onde o identificador de conexão da configuração é aceito como 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 o ID da conexão não for especificado, a conexão denominada “padrão” 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 tanto por meio de arquivos xml quanto por meio de classes. Para escrita conveniente de repositórios stub em testes unitários, existe uma biblioteca ViennaNET.TestUtils.Orm.

Exemplos completos de uso do ViennaNET.Orm.* podem ser encontrados aqui.

ViennaNET.Mensagens.*

Um conjunto de bibliotecas para trabalhar com filas.

Para trabalhar com filas, optou-se pela mesma abordagem dos diversos SGBDs, ou seja, a abordagem mais unificada possível em termos de trabalho com a biblioteca, independente do gerenciador de filas utilizado. Biblioteca ViennaNET.Messaging é precisamente responsável por esta unificação, e ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue и ViennaNET.Messaging.KafkaQueue contêm implementações de adaptador para IBM MQ, RabbitMQ e Kafka, respectivamente.

Ao trabalhar com filas, existem dois processos: receber uma mensagem e enviá-la.

Considere receber. Existem 2 opções aqui: para escuta contínua e para receber uma única mensagem. Para escutar constantemente a fila, você deve primeiro descrever a classe de processador herdada de IMessageProcessor, que será responsável pelo processamento da mensagem recebida. Em seguida, deve ser “vinculado” a uma fila específica; isso é feito através do cadastro no IQueueReactorFactory indicando o identificador da fila da configuração:

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

Exemplo de como começar a ouvir:

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

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

Para receber uma única mensagem em uma interface de fábrica IMessagingComponentFactory existe um método CreateMessageReceiverque criará um destinatário aguardando uma mensagem da fila especificada para ele:

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

Para enviar uma mensagem você precisa usar o mesmo IMessagingComponentFactory e crie um remetente de 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 facilmente fazer suas próprias implementações de interface IMessageSerializer и IMessageDeserializer.

Tentamos preservar os recursos exclusivos de cada gerenciador de filas, por exemplo. ViennaNET.Messaging.MQSeriesQueue permite que você envie não apenas texto, mas também mensagens de byte, e ViennaNET.Messaging.RabbitMQQueue suporta roteamento e enfileiramento instantâneo. Nosso wrapper de adaptador para RabbitMQ também implementa alguma aparência de RPC: enviamos uma mensagem e aguardamos uma resposta de uma fila temporária especial, que é criada apenas para uma mensagem de resposta.

aqui é um exemplo de uso de filas com nuances básicas de conexão.

ViennaNET.CallContext

Utilizamos filas não apenas para integração entre diferentes sistemas, mas também para comunicação entre microsserviços de uma mesma aplicação, por exemplo, dentro de uma saga. Isso levou à necessidade de transmitir junto com a mensagem dados auxiliares como login do usuário, identificador de solicitação para registro ponta a ponta, endereço IP de origem e dados de autorização. Para implementar o encaminhamento desses dados, desenvolvemos uma biblioteca ViennaNET.CallContext, que permite armazenar dados de uma solicitação que entra no serviço. Neste caso, não importa como a solicitação foi feita, através de fila ou via Http. Então, antes de enviar a 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 os dados auxiliares e os gerencia da mesma forma.

Obrigado pela sua atenção, aguardamos seus comentários e solicitações!

Fonte: habr.com

Adicionar um comentário