ViennaNET: un insieme di librerie per il backend. Parte 2

La comunità degli sviluppatori .NET della Raiffeisenbank continua a esaminare brevemente i contenuti di ViennaNET. Su come e perché siamo arrivati ​​a questo, puoi leggere la prima parte.

In questo articolo esamineremo le librerie ancora da prendere in considerazione per lavorare con transazioni distribuite, code e database, che possono essere trovate nel nostro repository GitHub (le fonti sono qui), e Pacchetti Nuget qui.

ViennaNET: un insieme di librerie per il backend. Parte 2

ViennaNET.Sagas

Quando un progetto passa all'architettura DDD e ai microservizi, quando la logica di business viene distribuita su diversi servizi, sorge un problema legato alla necessità di implementare un meccanismo di transazione distribuito, poiché molti scenari spesso interessano più domini contemporaneamente. Puoi conoscere tali meccanismi in modo più dettagliato, ad esempio, nel libro "Microservices Patterns", Chris Richardson.

Nei nostri progetti abbiamo implementato un meccanismo semplice ma utile: una saga, o meglio una saga basata sull'orchestrazione. La sua essenza è la seguente: esiste un determinato scenario aziendale in cui è necessario eseguire operazioni in sequenza in diversi servizi e, se si verificano problemi in qualsiasi passaggio, è necessario richiamare la procedura di rollback per tutti i passaggi precedenti, dove è fornito. Pertanto, alla fine della saga, indipendentemente dal successo, riceviamo dati coerenti in tutti i settori.

La nostra implementazione è ancora realizzata nella sua forma base e non è legata all'utilizzo di alcuna modalità di interazione con altri servizi. Non è difficile da usare: basta creare un discendente della classe astratta di base SagaBase<T>, dove T è la classe di contesto in cui è possibile memorizzare i dati iniziali necessari al funzionamento della saga, oltre ad alcuni risultati intermedi. L'istanza del contesto verrà passata a tutti i passaggi durante l'esecuzione. Saga stessa è una classe stateless, quindi l'istanza può essere inserita in DI come Singleton per ottenere le dipendenze necessarie.

Annuncio di esempio:

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

Chiamata di esempio:

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

È possibile visualizzare esempi completi di diverse implementazioni qui e in assemblea con test.

ViennaNET.Orm.*

Un insieme di librerie per lavorare con vari database tramite Nibernate. Utilizziamo l'approccio DB-First utilizzando Liquibase, quindi esiste solo la funzionalità per lavorare con i dati in un database già pronto.

ViennaNET.Orm.Seedwork и ViennaNET.Orm – assiemi principali contenenti rispettivamente le interfacce di base e le loro implementazioni. Vediamo più nel dettaglio il loro contenuto.

Interfaccia IEntityFactoryService e la sua implementazione EntityFactoryService sono il punto di partenza principale per lavorare con il database, poiché qui vengono creati l'Unità di lavoro, i repository per lavorare con entità specifiche, nonché gli esecutori di comandi e le query SQL dirette. A volte è conveniente limitare le capacità di una classe per lavorare con un database, ad esempio, per fornire solo la possibilità di leggere i dati. Per questi casi IEntityFactoryService c'è un'interfaccia antenato IEntityRepositoryFactory, che dichiara solo un metodo per la creazione di repository.

Per accedere direttamente al database viene utilizzato il meccanismo del provider. Ogni DBMS che utilizziamo nei nostri team ha la propria implementazione: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Allo stesso tempo, più fornitori possono essere registrati contemporaneamente in un'applicazione, il che consente, ad esempio, nell'ambito di un servizio, senza costi per la modifica dell'infrastruttura, di eseguire una migrazione passo dopo passo da un DBMS all'altro. Il meccanismo per selezionare la connessione richiesta e, quindi, il provider per una specifica classe di entità (per la quale è scritta la mappatura sulle tabelle del database) viene implementato attraverso la registrazione dell'entità nella classe BoundedContext (contiene un metodo per registrare le entità di dominio) o il suo successore ApplicationContext (contiene metodi per la registrazione di entità dell'applicazione, richieste dirette e comandi), dove l'identificatore di connessione dalla configurazione è accettato come argomento:

"db": [
  {
    "nick": "mssql_connection",
    "dbServerType": "MSSQL",
    "ConnectionString": "...",
    "useCallContext": true
  },
  {
    "nick": "oracle_connection",
    "dbServerType": "Oracle",
    "ConnectionString": "..."
  }
],

Esempio di contesto applicativo:

internal sealed class DbContext : ApplicationContext
{
  public DbContext()
  {
    AddEntity<SomeEntity>("mssql_connection");
    AddEntity<MigratedSomeEntity>("oracle_connection");
    AddEntity<AnotherEntity>("oracle_connection");
  }
}

Se l'ID di connessione non è specificato, verrà utilizzata la connessione denominata "predefinita".

La mappatura diretta delle entità sulle tabelle del database viene implementata utilizzando gli strumenti standard di NHibernate. È possibile utilizzare la descrizione sia tramite file xml che tramite classi. Per una comoda scrittura dei repository stub negli Unit test, è disponibile una libreria ViennaNET.TestUtils.Orm.

È possibile trovare esempi completi di utilizzo di ViennaNET.Orm.* qui.

ViennaNET.Messaging.*

Un insieme di librerie per lavorare con le code.

Per lavorare con le code, è stato scelto lo stesso approccio dei vari DBMS, ovvero l'approccio unificato massimo possibile in termini di lavoro con la libreria, indipendentemente dal gestore code utilizzato. Biblioteca ViennaNET.Messaging è proprio responsabile di questa unificazione, e ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue и ViennaNET.Messaging.KafkaQueue contengono rispettivamente implementazioni dell'adattatore per IBM MQ, RabbitMQ e Kafka.

Quando si lavora con le code, esistono due processi: ricevere un messaggio e inviarlo.

Considera la possibilità di ricevere. Qui ci sono 2 opzioni: per l'ascolto continuo e per la ricezione di un singolo messaggio. Per ascoltare costantemente la coda, devi prima descrivere la classe del processore ereditata da IMessageProcessor, che sarà responsabile dell'elaborazione del messaggio in arrivo. Successivamente deve essere “legato” ad una coda specifica; ciò avviene tramite la registrazione in IQueueReactorFactory indicando l'identificativo della coda dalla configurazione:

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

Esempio di avvio dell'ascolto:

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

Quindi, quando il servizio viene avviato e il metodo viene chiamato per iniziare l'ascolto, tutti i messaggi dalla coda specificata verranno inviati al processore corrispondente.

Per ricevere un singolo messaggio in un'interfaccia di fabbrica IMessagingComponentFactory c'è un metodo CreateMessageReceiverche creerà un destinatario in attesa di un messaggio dalla coda specificata:

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

Per inviare un messaggio devi usare lo stesso IMessagingComponentFactory e creare un mittente del messaggio:

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

Esistono tre opzioni già pronte per serializzare e deserializzare un messaggio: solo testo, XML e JSON, ma se necessario puoi facilmente realizzare le tue implementazioni dell'interfaccia IMessageSerializer и IMessageDeserializer.

Abbiamo cercato di preservare le capacità uniche di ciascun gestore code, ad es. ViennaNET.Messaging.MQSeriesQueue ti consente di inviare non solo testo, ma anche messaggi di byte e ViennaNET.Messaging.RabbitMQQueue supporta il routing e l'accodamento al volo. Il nostro wrapper adattatore per RabbitMQ implementa anche una parvenza di RPC: inviamo un messaggio e aspettiamo una risposta da una coda temporanea speciale, che viene creata solo per un messaggio di risposta.

Qui un esempio di utilizzo delle code con sfumature di connessione di base.

ViennaNET.CallContext

Utilizziamo le code non solo per l'integrazione tra sistemi diversi, ma anche per la comunicazione tra microservizi della stessa applicazione, ad esempio all'interno di una saga. Ciò ha portato alla necessità di trasmettere insieme al messaggio dati ausiliari come il login dell'utente, l'identificatore della richiesta per la registrazione end-to-end, l'indirizzo IP di origine e i dati di autorizzazione. Per implementare l'inoltro di questi dati, abbiamo sviluppato una libreria ViennaNET.CallContext, che consente di memorizzare i dati di una richiesta che entra nel servizio. In questo caso non ha importanza come è stata effettuata la richiesta, tramite coda o tramite Http. Quindi, prima di inviare la richiesta o il messaggio in uscita, i dati vengono presi dal contesto e inseriti nelle intestazioni. Pertanto, il servizio successivo riceve i dati ausiliari e li gestisce allo stesso modo.

Grazie per l'attenzione, attendiamo con ansia i vostri commenti e le vostre richieste di pull!

Fonte: habr.com

Aggiungi un commento