ViennaNET: un set de biblioteci pentru backend. Partea 2

Comunitatea dezvoltatorilor .NET de la Raiffeisen Bank continuă analiza succintă a conținutului ViennaNET. Despre cum și de ce am ajuns la acest punct, puteți citi în prima parte.

În acest articol vom trece în revistă bibliotecile încă neexplorate pentru gestionarea tranzacțiilor distribuite, cozile și bazele de date, care pot fi găsite în repositoarele noastre de pe GitHub (sursa se află aici), iar pachetele Nuget sunt aici.

ViennaNET: un set de biblioteci pentru backend. Partea 2

ViennaNET.Sagas

Atunci când un proiect trece la DDD și arhitectura microserviciilor, la despărțirea logicii de afaceri în servicii diferite, apare problema necesității implementării unui mecanism de tranzacții distribuite, deoarece multe scenarii implică adesea mai multe domenii. Aceste mecanisme pot fi studiate mai în detaliu, de exemplu, în cartea „Microservices Patterns”, Chris Richardson.

În proiectele noastre am implementat un mecanism simplu, dar util: saga, mai precis o saga bazată pe orchestrare. Conceptul ei este următorul: există un anumit scenariu de afaceri în care este necesară efectuarea secvențială a operațiunilor în diferite servicii, iar în cazul în care apar probleme în orice etapă, este necesar să se invoce o procedură de returnare a tuturor pașilor anteriori, acolo unde este prevăzută. Astfel, la finalul execuției sagelor, indiferent de succes, obținem date coerente în toate domeniile.

Implementarea noastră este deocamdată realizată într-o formă de bază și nu depinde de utilizarea unor metode de interacțiune cu alte servicii. Aplicarea ei nu este dificilă: este suficient să creați un derivat al clasei abstracte de bază SagaBase<T>, unde T este clasa voastră de context, în care pot fi stocate datele inițiale necesare pentru funcționarea sagelor, precum și unele rezultate intermediare. Instanța contextului va fi transmisă în toate pașii în timpul execuției. Saga în sine este o clasă stateless, deci instanța poate fi injectată în DI ca Singleton, pentru a obține dependențele necesare.

Exemplu de declarație:

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

Exemplu de apel:

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

Exemple complete ale diferitelor implementări pot fi vizualizate aici și în colecția cu teste.

ViennaNET.Orm.*

Pachet de biblioteci pentru lucrul cu diverse baze de date prin Nhibernate. Utilizăm abordarea DB-First cu aplicarea Liquibase, așa că aici se găsește doar funcționalitatea de lucru cu datele într-o bază de date existentă.

ViennaNET.Orm.Seedwork și ViennaNET.Orm – sunt principalele biblioteci, care conțin interfețele de bază și implementările lor, respectiv. Vom detalia conținutul acestora.

Interfața IEntityFactoryService și implementarea sa EntityFactoryService sunt punctul principal de plecare pentru lucrul cu baza de date, deoarece aici se creează Unit of Work, repozitoarele pentru lucrul cu entitățile specifice, precum și executorii de comenzi și interogările SQL directe. Uneori, este convenabil să limităm capacitățile clasei care lucrează cu baza de date, de exemplu, permitând doar citirea datelor. În astfel de cazuri, IEntityFactoryService există un părintele – interfața IEntityRepositoryFactory, în care este declarat doar metoda pentru crearea repozitoarelor.

Pentru accesul direct la baza de date se utilizează mecanismul provider-ilor. Fiecare sistem de management al bazei de date utilizat în echipele noastre are propria sa implementare: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Astfel, într-o aplicație pot fi înregistrate simultan mai multe provider-i, ceea ce permite, de exemplu, în cadrul unui singur serviciu, fără costuri suplimentare pentru îmbunătățirea infrastructurii, să se efectueze o migrare treptată de la un SGBD la altul. Mecanismul de selecție a conexiunii necesare și, prin urmare, a provider-ului pentru o anumită clasă-entitate (pentru care se scrie mapping-ul în tabelele bazei de date) este realizat prin înregistrarea entității în clasa BoundedContext (care conține metoda pentru înregistrarea entităților de domeniu) sau a moștenitorului său ApplicationContext (care conține metode pentru înregistrarea entităților aplicației, interogărilor directe și comenzilor), unde ca argument se acceptă identificatorul conexiunii din configurație:

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

Exemplu de ApplicationContext:

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

Dacă identificatorul conexiunii nu este specificat, atunci se va utiliza conexiunea cu numele „default“.

Maparea entităților pe tabelele Bazei de Date se realizează prin intermediul mecanismelor standard NHibernate. Se poate utiliza descrierea atât prin fișiere XML, cât și prin clase. Pentru a facilita redactarea de repozitorii simulare în testele Unit, există o bibliotecă ViennaNET.TestUtils.Orm.

Exemple complete de utilizare a ViennaNET.Orm.* pot fi găsite aici.

ViennaNET.Messaging.*

Un set de biblioteci pentru lucrul cu cozi.

Pentru lucrul cu cozi a fost aleasă aceeași abordare ca și cu diverse SGBD-uri, și anume – o abordare cât mai unificată din punct de vedere al interacțiunii cu biblioteca, indiferent de managerul de cozi utilizat. Biblioteca ViennaNET.Messaging se ocupă exact cu această unificare, iar ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue și ViennaNET.Messaging.KafkaQueue conțin implementări ale adaptorilor pentru IBM MQ, RabbitMQ și Kafka, respectiv.

În lucrul cu cozi există două procese: primirea mesajului și trimiterea.

Să analizăm primirea. Aici sunt 2 variante: pentru ascultare continuă și pentru primirea unui singur mesaj. Pentru ascultare continuă a cozii, trebuie mai întâi să descriem o clasă procesor, derivată din IMessageProcessor, care se va ocupa de procesarea mesajului primit. Apoi, este necesar să o «legăm» de o anumită coadă, ceea ce se face prin înregistrarea în IQueueReactorFactory cu specificarea identificatorului cozii din configurație:

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

Exemplul de pornire a ascultării:

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

Apoi, la pornirea serviciului și apelarea metodei pentru a începe ascultarea, toate mesajele din coada specificată vor ajunge la procesorul corespunzător.

Pentru a primi un mesaj singular în fabrica de interfețe IMessagingComponentFactory există metoda CreateMessageReceiver, care va crea un receptor care așteaptă mesaj din coada specificată:

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

Pentru a trimite un mesaj este necesar să utilizați aceeași metodă IMessagingComponentFactory și să creați un expeditor de mesaje:

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

Pentru serializarea și deserializarea mesajelor, există trei opțiuni gata făcute: text simplu, XML și JSON, dar dacă este necesar, se pot realiza cu ușurință propriile implementări ale interfețelor. IMessageSerializer și IMessageDeserializer.

Am încercat să păstrăm oportunitățile unice ale fiecărui manager de cozi, de exemplu, ViennaNET.Messaging.MQSeriesQueue permite trimiterea nu doar de mesaje text, ci și de mesaje binare, iar ViennaNET.Messaging.RabbitMQQueue susține rutarea și crearea de cozi "din mers". În wrapperul nostru de adaptor pentru RabbitMQ, s-a implementat de asemenea un fel de RPC: trimitem un mesaj și așteptăm un răspuns dintr-o coadă temporară specială, care este creată doar pentru un singur mesaj de răspuns.

Iată un exemplu de utilizare a cozilor cu principalele nuanțe de conectare.

ViennaNET.CallContext

Folosim cozile nu doar pentru integrarea între diferite sisteme, ci și pentru comunicarea între microservicii ale unei singure aplicații, de exemplu, în cadrul unei sagă. Aceasta a dus la necesitatea de a transmite împreună cu mesajul date auxiliare, cum ar fi numele de utilizator, identificatorul solicitării pentru logging-ul complet, adresa IP a sursei și datele de autorizare. Pentru a implementa transferul acestor date, am dezvoltat o bibliotecă ViennaNET.CallContext, care permite stocarea datelor din solicitarea de intrare în serviciu. În acest fel, modul în care a fost făcută solicitarea, prin coadă sau prin Http, nu are importanță. Apoi, înainte de a trimite solicitarea de ieșire sau mesajul, se extrag datele din context și se pun în anteturi. Astfel, următorul serviciu primește datele auxiliare și le gestionează în mod similar.

Mulțumim pentru atenție, așteptăm comentariile și pull request-urile dumneavoastră!

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster