La comunità degli sviluppatori .NET di Raiffeisenbank continua a fornire una breve panoramica del contenuto di ViennaNET. Per scoprire come e perché siamo arrivati a questo, .
In questo articolo esamineremo altre librerie non ancora trattate per lavorare con transazioni distribuite, code e database, che puoi trovare nel nostro repository su GitHub (), e .
ViennaNET.Sagas
Quando un progetto passa a DDD e architettura a microservizi, si presenta il problema della necessità di implementare un meccanismo per transazioni distribuite, poiché molti scenari coinvolgono spesso più domini. Puoi approfondire questi meccanismi, per esempio, .
Nei nostri progetti abbiamo implementato un meccanismo semplice ma utile: una saga, o meglio, una saga basata sull'architettura. La sua essenza è la seguente: esiste uno scenario di business in cui è necessario eseguire operazioni in sequenza in diversi servizi. In caso di problemi in qualsiasi fase, è necessario attivare una procedura di rollback di tutti i passaggi precedenti, dove previsto. Pertanto, al termine dell'esecuzione della saga, indipendentemente dal successo, otteniamo dati coerenti in tutti i domini.
La nostra implementazione è attualmente realizzata in forma di base e non è legata all'uso di alcun metodo di interazione con altri servizi. Applicarlo non è difficile: basta creare un erede della classe astratta di base SagaBase, dove T rappresenta la tua classe di contesto, in cui è possibile memorizzare i dati sorgente necessari per il funzionamento della saga, nonché alcuni risultati intermedi. Un'istanza del contesto sarà passata a tutti i passaggi durante l'esecuzione. La saga stessa è una classe stateless, quindi l'istanza può essere registrata nel DI come Singleton per ottenere le dipendenze necessarie.
Esempio di dichiarazione:
public class ExampleSaga : SagaBase
{
public ExampleSaga()
{
Step("Step 1")
.WithAction(c => ...)
.WithCompensation(c => ...);
AsyncStep("Step 2")
.WithAction(async c => ...);
}
}
Esempio di chiamata:
var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);
Esempi completi di diverse implementazioni possono essere visti anche nella collezione di .
ViennaNET.Orm.*
Un insieme di librerie per lavorare con vari database tramite Nhibernate. Noi utilizziamo un approccio DB-First con l'uso di Liquibase, quindi qui è presente solo la funzionalità per lavorare con i dati in un database esistente.
ViennaNET.Orm.Seedwork e ViennaNET.Orm sono gli assemblaggi principali, contenenti le interfacce di base e le loro implementazioni rispettivamente. Esaminiamo più in dettaglio il loro contenuto.
Interfaccia IEntityFactoryService e la sua implementazione EntityFactoryService sono il principale punto di partenza per lavorare con il database, poiché qui viene creato l'Unit of Work, i repository per lavorare con entità specifiche, nonché gli esecutori di comandi e query SQL dirette. A volte è utile limitare le capacità di una classe per lavorare con il database, ad esempio consentendo solo la lettura dei dati. In questi casi, IEntityFactoryService c'è un genitore – l'interfaccia IEntityRepositoryFactory, in cui è dichiarato solo il metodo per la creazione di repository.
Per l'accesso diretto al database viene utilizzato il meccanismo dei provider. Per ogni DBMS che utilizziamo nelle nostre squadre, esiste una propria implementazione: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.
In un'applicazione possono essere registrati contemporaneamente più provider, il che consente, ad esempio, di effettuare una migrazione graduale da un DBMS a un altro, senza costi aggiuntivi per la modifica dell'infrastruttura. Il meccanismo per scegliere la connessione necessaria e, di conseguenza, il provider per una specifica entità (per la quale si sta scrivendo il mapping delle tabelle del database) è implementato attraverso la registrazione dell'entità nella classe BoundedContext (contiene il metodo per la registrazione delle entità di dominio) o nel suo erede ApplicationContext (contiene metodi per la registrazione delle entità applicative, delle query dirette e dei comandi), dove come argomento viene accettuto l'identificativo della connessione dalla configurazione:
"db": [
{
"nick": "mssql_connection",
"dbServerType": "MSSQL",
"ConnectionString": "...",
"useCallContext": true
},
{
"nick": "oracle_connection",
"dbServerType": "Oracle",
"ConnectionString": "..."
}
],
Esempio di ApplicationContext:
internal sealed class DbContext : ApplicationContext
{
public DbContext()
{
AddEntity("mssql_connection");
AddEntity("oracle_connection");
AddEntity("oracle_connection");
}
}
Se l'identificativo della connessione non è specificato, verrà utilizzata la connessione chiamata 'default'.
Il mapping delle entità alle tabelle del database viene realizzato con i mezzi standard di NHibernate. È possibile utilizzare la descrizione sia tramite file XML che tramite classi. Per facilitare la scrittura di repository stub nei test unitari, è disponibile una libreria ViennaNET.TestUtils.Orm.
Esempi completi di utilizzo di ViennaNET.Orm.* possono essere trovati .
ViennaNET.Messaging.*
Una serie di librerie per lavorare con le code.
Per la gestione delle code è stato scelto lo stesso approccio utilizzato per i vari DBMS, ovvero un approccio il più possibile unificato dal punto di vista del lavoro con la libreria, indipendentemente dal gestore di code utilizzato. La libreria ViennaNET.Messaging è proprio quella che risponde a questa unificazione, mentre ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue e ViennaNET.Messaging.KafkaQueue contengono implementazioni degli adattatori per IBM MQ, RabbitMQ e Kafka rispettivamente.
Nel lavoro con le code ci sono due processi: ricezione del messaggio e invio.
Consideriamo la ricezione. Qui ci sono 2 varianti: per ascolto continuo e per la ricezione di un singolo messaggio. Per l'ascolto continuo della coda è necessario innanzitutto descrivere una classe processore, ereditata da IMessageProcessor, che sarà responsabile del trattamento del messaggio in arrivo. Successivamente, deve essere "collegata" a una certa coda, ciò avviene tramite la registrazione in IQueueReactorFactory specificando l'identificatore della coda dalla configurazione:
"messaging": {
"ApplicationName": "MyApplication"
},
"rabbitmq": {
"queues": [
{
"id": "myQueue",
"queuename": "lalala",
...
}
]
},
Esempio di avvio dell'ascolto:
_queueReactorFactory.Register("myQueue");
var queueReactor = queueReactorFactory.CreateQueueReactor("myQueue");
queueReactor.StartProcessing();
Dopo, all'avvio del servizio e alla chiamata del metodo per iniziare l'ascolto, tutti i messaggi dalla coda specificata verranno diretti al relativo processore.
Per ricevere un singolo messaggio nell'interfaccia-fabbrica IMessagingComponentFactory c'è il metodo CreateMessageReceiver, che creerà un ricevitore in attesa di messaggi dalla coda che gli è stata indicata:
using (var receiver = _messagingComponentFactory.CreateMessageReceiver("myQueue"))
{
var message = receiver.Receive();
}
Per inviare un messaggio è necessario utilizzare la stessa IMessagingComponentFactory e creare un mittente di messaggi:
using (var sender = _messagingComponentFactory.CreateMessageSender("myQueue"))
{
sender.SendMessage(new MyMessage { Value = ...});
}
Per la serializzazione e deserializzazione del messaggio sono disponibili tre varianti pronte: semplice testo, XML e JSON, ma se necessario è facile fare le proprie implementazioni delle interfacce IMessageSerializer e IMessageDeserializer.
Abbiamo cercato di mantenere le caratteristiche uniche di ogni gestore di code, ad esempio, ViennaNET.Messaging.MQSeriesQueue consente di inviare non solo messaggi testuali, ma anche messaggi byte, mentre ViennaNET.Messaging.RabbitMQQueue supporta il routing e la creazione di code "al volo“. Nella nostra interfaccia di adattatore per RabbitMQ è anche stata implementata una sorta di RPC: inviamo un messaggio e aspettiamo una risposta da una coda temporanea speciale, che viene creata solo per un singolo messaggio di risposta.
Ecco .
ViennaNET.CallContext
Utilizziamo le code non solo per l'integrazione tra diversi sistemi, ma anche per la comunicazione tra i microservizi di una stessa applicazione, ad esempio, nel contesto di una saga. Questo ha portato alla necessità di trasmettere insieme al messaggio informazioni ausiliarie come il login dell'utente, l'identificatore della richiesta per il logging trasversale, l'indirizzo IP sorgente e i dati di autorizzazione. Per realizzare il passaggio di questi dati, è stata sviluppata una libreria ViennaNET.CallContext, che consente di memorizzare i dati dalla richiesta in ingresso al servizio. In questo caso, non conta come è stata effettuata la richiesta, tramite coda o tramite Http. Poi, prima di inviare la richiesta in uscita o il messaggio, i dati vengono recuperati dal contesto e posizionati nelle intestazioni. In questo modo, il servizio successivo riceve i dati ausiliari e li gestisce analogamente.
Grazie per l'attenzione, attendiamo i vostri commenti e pull request!
Fonte: habr.com
