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

La comunità degli sviluppatori .NET di Raiffeisenbank continua un'analisi concisa del contenuto di ViennaNET. Su come e perché siamo arrivati a questo, è possibile leggere nella prima parte..

In questo articolo esamineremo ulteriori librerie per lavorare con transazioni distribuite, code e basi di dati, che possono essere trovate nel nostro repository su GitHub (i sorgenti si trovano qui), e I pacchetti Nuget sono qui.

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

ViennaNET.Sagas

Quando un progetto passa a DDD e architettura a microservizi, si presenta il problema di implementare un meccanismo per le transazioni distribuite, poiché molti scenari coinvolgono spesso più domini. Con questi meccanismi è possibile approfondire, ad esempio, nel libro «Microservices Patterns», Chris Richardson..

Nei nostri progetti abbiamo implementato un meccanismo semplice ma utile: la saga, o più precisamente, una saga basata sull' orchestrazione. La sua essenza è la seguente: c'è uno specifico scenario aziendale in cui è necessario eseguire operazioni in sequenza su diversi servizi; in caso di problemi in qualsiasi fase, è necessario attivare la procedura di rollback di tutti i passaggi precedenti, dove previsto. Così, al termine dell'esecuzione della saga, a prescindere dal successo, otteniamo dati consistenti in tutti i domini.

La nostra implementazione è al momento semplice e non legata all'uso di modalità di interazione con altri servizi. Applicarla non è difficile: è sufficiente creare un’origine dal class base astratta SagaBase, dove T è la vostra classe di contesto, in cui è possibile memorizzare i dati sorgente necessari per il funzionamento della saga, oltre ad alcuni risultati intermedi. L'istanza del contesto sarà trasmessa in tutti i passaggi durante l'esecuzione. La saga stessa è una classe stateless, quindi l'istanza può essere collocata in 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 qui e nella raccolta con test.

ViennaNET.Orm.*

Un insieme di librerie per lavorare con diversi DB attraverso Nhibernate. Utilizziamo un approccio DB-First con l'applicazione di Liquibase, quindi qui è presente solo il funzionalità per lavorare con i dati in un DB già esistente.

ViennaNET.Orm.Seedwork e ViennaNET.Orm sono i principali assembly che contengono le interfacce di base e le loro implementazioni rispettivamente. Approfondiremo meglio il loro contenuto.

Document IEntityFactoryService e la sua implementazione EntityFactoryService rappresentano il punto di partenza principale per lavorare con il DB, poiché qui viene creato il 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à della classe di lavorare con il DB, ad esempio, consentendo solo la lettura dei dati. Per questi casi, IEntityFactoryService ha un genitore - l'interfaccia IEntityRepositoryFactory, in cui è dichiarato solo il metodo per creare repository.

Per l'accesso diretto al DB viene utilizzato il meccanismo dei provider. Per ciascun DBMS che utilizziamo nelle nostre squadre, c'è la propria implementazione: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

In un'applicazione possono essere registrati più provider contemporaneamente, il che consente, ad esempio, di eseguire una migrazione graduale da un DBMS a un altro all'interno di un unico servizio senza costi aggiuntivi per modifiche all'infrastruttura. Il meccanismo per scegliere la connessione necessaria e, di conseguenza, il provider per una specifica classe-entità (per la quale viene scritta la mappatura sulle tabelle del DB) è implementato attraverso la registrazione dell'entità nella classe BoundedContext (che contiene un metodo per la registrazione delle entità dominanti) o nella sua sottoclasse ApplicationContext (che contiene metodi per la registrazione delle entità applicative, delle query dirette e dei comandi), dove come argomento viene accettuto l'identificatore di 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'identificatore di connessione non è specificato, verrà utilizzata la connessione con il nome «default».

Il mapping diretto delle entità alle tabelle del database viene realizzato con gli strumenti 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 qui.

ViennaNET.Messaging.*

Un insieme di librerie per lavorare con le code.

Per lavorare con le code è stato scelto lo stesso approccio utilizzato con diverse DBMS, ovvero un approccio il più possibile unificato dal punto di vista dell’utilizzo della libreria, indipendentemente dal manager di coda utilizzato. La libreria ViennaNET.Messaging risponde proprio a questa unificazione, mentre ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue e ViennaNET.Messaging.KafkaQueue contengono implementazioni degli adapter per IBM MQ, RabbitMQ e Kafka rispettivamente.

Nel lavoro con le code ci sono due processi: ricezione dei messaggi e invio.

Esaminiamo la ricezione. Ci sono 2 opzioni: per l’ascolto continuo e per la ricezione di un singolo messaggio. Per l’ascolto continuo della coda è necessario innanzitutto definire una classe processore, ereditata da IMessageProcessor, che si occuperà dell’elaborazione del messaggio in entrata. Successivamente, deve essere "collegata" a una determinata coda, ciò avviene attraverso la registrazione in IQueueReactorFactory specificando l’identificativo 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();

Quindi, all'avvio del servizio e alla chiamata del metodo per iniziare l'ascolto, tutti i messaggi dalla coda specificata verranno inviati al processore corrispondente.

Per ricevere un singolo messaggio, nell'interfaccia-fabbrica IMessagingComponentFactory c'è il metodo CreateMessageReceiver, che creerà un ricevitore che attende messaggi dalla coda specificata:

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 dei messaggi ci sono tre opzioni pronte: testo semplice, XML e JSON, ma se necessario è possibile realizzare senza problemi le proprie implementazioni delle interfacce IMessageSerializer e IMessageDeserializer.

Ci siamo sforzati di mantenere le uniche possibilità di ciascun gestore di code, ad esempio, ViennaNET.Messaging.MQSeriesQueue permette di inviare non solo messaggi di testo, ma anche messaggi in byte, mentre ViennaNET.Messaging.RabbitMQQueue supporta il routing e la creazione di code “al volo”. Nella nostra wrapper dell'adattatore per RabbitMQ è stata implementata anche una sorta di RPC: inviamo un messaggio e attendiamo una risposta da una coda temporanea speciale, che viene creata solo per un singolo messaggio di risposta.

Ecco esempio di utilizzo delle code con le principali sfide di connessione.

ViennaNET.CallContext

Utilizziamo le code non solo per l'integrazione tra diversi sistemi, ma anche per la comunicazione tra microservizi di una singola applicazione, ad esempio, nell'ambito di una saga. Ciò ha portato alla necessità di trasferire insieme al messaggio dati ausiliari come il login dell'utente, l'identificatore della richiesta per il logging incrociato, l'indirizzo IP della fonte e i dati di autorizzazione. Per implementare il passaggio di questi dati abbiamo sviluppato una libreria ViennaNET.CallContext, che consente di memorizzare i dati dalla richiesta in entrata verso il servizio. In questo modo, il modo in cui è stata effettuata la richiesta, tramite coda o tramite Http, non ha importanza. Poi, prima di inviare la richiesta o il messaggio in uscita, i dati vengono estratti dal contesto e inseriti nelle intestazioni. In questo modo, il servizio successivo riceve i dati ausiliari e li utilizza di conseguenza.

Grazie per l'attenzione, attendiamo i vostri commenti e pull request!

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster