ViennaNET: un set de biblioteci pentru backend. Partea 2

Comunitatea de dezvoltatori Raiffeisenbank .NET continuă să revizuiască pe scurt conținutul ViennaNET. Despre cum și de ce am ajuns la asta, puteti citi prima parte.

În acest articol, vom parcurge biblioteci care nu sunt încă considerate pentru a lucra cu tranzacții distribuite, cozi și baze de date, care pot fi găsite în depozitul nostru GitHub (sursele sunt aici) și Pachetele Nuget aici.

ViennaNET: un set de biblioteci pentru backend. Partea 2

ViennaNET.Sagas

Atunci când un proiect trece la arhitectura DDD și microservicii, atunci când logica de afaceri este distribuită între diferite servicii, apare o problemă legată de necesitatea implementării unui mecanism de tranzacție distribuită, deoarece multe scenarii afectează adesea mai multe domenii simultan. Puteți face cunoștință cu astfel de mecanisme mai detaliat, de exemplu, în cartea „Microservices Patterns”, Chris Richardson.

În proiectele noastre, am implementat un mecanism simplu, dar util: o saga, sau mai degrabă o saga bazată pe orchestrație. Esența sa este următoarea: există un anumit scenariu de afaceri în care este necesar să se efectueze secvențial operațiuni în diferite servicii, iar în cazul oricăror probleme la orice pas, este necesar să se apeleze procedura de rollback a tuturor pașilor anteriori, unde este furnizat. Astfel, la finalul sagăi, indiferent de succes, primim date consistente în toate domeniile.

Implementarea noastră este încă realizată în forma sa de bază și nu este legată de utilizarea vreunei metode de interacțiune cu alte servicii. Nu este dificil de folosit: trebuie doar să faci un descendent al clasei abstracte de bază SagaBase<T>, unde T este clasa ta de context în care poți stoca datele inițiale necesare pentru ca saga să funcționeze, precum și câteva rezultate intermediare. Instanța de context va fi transmisă la toți pașii în timpul execuției. Saga în sine este o clasă fără stat, astfel încât instanța poate fi plasată în DI ca Singleton pentru a obține dependențele necesare.

Exemplu de anunț:

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);

Pot fi vizualizate exemple complete de implementări diferite aici iar în asamblare cu teste.

ViennaNET.Orm.*

Un set de biblioteci pentru lucrul cu diferite baze de date prin Nhibernate. Folosim abordarea DB-First folosind Liquibase, deci există doar funcționalitate pentru lucrul cu date într-o bază de date gata făcută.

ViennaNET.Orm.Seedwork и ViennaNET.Orm – ansambluri principale care conțin interfețe de bază și, respectiv, implementările acestora. Să ne uităm la conținutul lor mai detaliat.

interfață IEntityFactoryService și implementarea acesteia EntityFactoryService sunt principalul punct de plecare pentru lucrul cu baza de date, din moment ce Unitatea de lucru, depozitele pentru lucrul cu entități specifice, precum și executanții de comenzi și interogări SQL directe sunt create aici. Uneori este convenabil să se limiteze capacitățile unei clase de lucru cu o bază de date, de exemplu, pentru a oferi capacitatea de a citi doar date. Pentru astfel de cazuri IEntityFactoryService există un strămoș - interfață IEntityRepositoryFactory, care declară doar o metodă de creare a depozitelor.

Pentru a accesa direct baza de date, se folosește mecanismul furnizorului. Fiecare SGBD pe care îl folosim în echipele noastre are propria sa implementare: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

În același timp, mai mulți furnizori pot fi înregistrați într-o aplicație în același timp, ceea ce permite, de exemplu, în cadrul unui singur serviciu, fără costuri pentru modificarea infrastructurii, să efectueze o migrare pas cu pas din de la un DBMS la altul. Mecanismul de selectare a conexiunii necesare și, prin urmare, a furnizorului pentru o anumită clasă de entități (pentru care se scrie maparea la tabelele bazei de date) este implementat prin înregistrarea entității în clasa BoundedContext (conține o metodă de înregistrare a entităților de domeniu) sau a succesorului acesteia. ApplicationContext (conține metode pentru înregistrarea entităților aplicației, solicitări directe și comenzi), unde identificatorul de conexiune din configurație este acceptat ca argument:

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

Exemplu ApplicationContext:

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

Dacă ID-ul conexiunii nu este specificat, atunci conexiunea numită „implicit” va fi utilizată.

Maparea directă a entităților la tabelele bazei de date este implementată folosind instrumente standard NHibernate. Puteți folosi descrierea atât prin fișiere xml, cât și prin clase. Pentru scrierea convenabilă a depozitelor stub în testele unitare, există o bibliotecă ViennaNET.TestUtils.Orm.

Pot fi găsite exemple complete de utilizare a ViennaNET.Orm.* aici.

ViennaNET.Messaging.*

Un set de biblioteci pentru lucrul cu cozile.

Pentru a lucra cu cozi s-a ales aceeași abordare ca la diferite SGBD-uri și anume abordarea unificată maximă posibilă în ceea ce privește lucrul cu biblioteca, indiferent de managerul de cozi utilizat. Bibliotecă ViennaNET.Messaging este tocmai responsabil pentru această unificare și ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue и ViennaNET.Messaging.KafkaQueue conțin implementări de adaptoare pentru IBM MQ, RabbitMQ și, respectiv, Kafka.

Când lucrați cu cozi, există două procese: primirea unui mesaj și trimiterea acestuia.

Luați în considerare primirea. Există 2 opțiuni aici: pentru ascultare continuă și pentru primirea unui singur mesaj. Pentru a asculta constant coada, trebuie mai întâi să descrii clasa procesorului de la care a fost moștenit IMessageProcessor, care va fi responsabil pentru procesarea mesajului primit. Apoi, trebuie să fie „legat” la o anumită coadă; acest lucru se face prin înregistrare în IQueueReactorFactory indicând identificatorul de coadă din configurație:

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

Exemplu de începere a ascultarii:

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

Apoi, când serviciul pornește și metoda este apelată pentru a începe ascultarea, toate mesajele din coada specificată vor merge la procesorul corespunzător.

Pentru a primi un singur mesaj într-o interfață din fabrică IMessagingComponentFactory exista o metoda CreateMessageReceivercare va crea un destinatar care așteaptă un mesaj din coada specificată de acesta:

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

Pentru a trimite un mesaj trebuie să folosești același lucru IMessagingComponentFactory și creați un expeditor de mesaj:

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

Există trei opțiuni gata făcute pentru serializarea și deserializarea unui mesaj: doar text, XML și JSON, dar dacă este necesar, puteți face cu ușurință propriile implementări de interfață IMessageSerializer и IMessageDeserializer.

Am încercat să păstrăm capacitățile unice ale fiecărui manager de cozi, de ex. ViennaNET.Messaging.MQSeriesQueue vă permite să trimiteți nu numai mesaje text, ci și mesaje pe octeți și ViennaNET.Messaging.RabbitMQQueue acceptă rutarea și coada de așteptare din mers. Învelișul adaptorului nostru pentru RabbitMQ implementează, de asemenea, o aparență de RPC: trimitem un mesaj și așteptăm un răspuns de la o coadă temporară specială, care este creată doar pentru un mesaj de răspuns.

Aici un exemplu de utilizare a cozilor cu nuanțe de bază ale conexiunii.

ViennaNET.CallContext

Folosim cozi nu doar pentru integrarea între sisteme diferite, ci și pentru comunicarea între microservicii ale aceleiași aplicații, de exemplu, în cadrul unei saga. Acest lucru a condus la necesitatea transmiterii împreună cu mesajul unor date auxiliare precum autentificarea utilizatorului, identificatorul cererii pentru înregistrarea de la capăt la capăt, adresa IP sursă și datele de autorizare. Pentru a implementa transmiterea acestor date, am dezvoltat o bibliotecă ViennaNET.CallContext, care vă permite să stocați date dintr-o solicitare de intrare în serviciu. În acest caz, modul în care a fost făcută cererea, printr-o coadă sau prin Http, nu contează. Apoi, înainte de a trimite cererea sau mesajul de ieșire, datele sunt preluate din context și plasate în anteturi. Astfel, următorul serviciu primește datele auxiliare și le gestionează în același mod.

Vă mulțumim pentru atenție, așteptăm cu nerăbdare comentariile și solicitările voastre!

Sursa: www.habr.com

Adauga un comentariu