Fællesskabet af .NET-udviklere i Raiffeisenbank fortsætter med en kort gennemgang af indholdet i ViennaNET. Her kan du læse om, hvordan og hvorfor vi kom hertil, .
I denne artikel vil vi se nærmere på de endnu ikke behandlede biblioteker til håndtering af distribuerede transaktioner, køer og databaser, som kan findes i vores repository på GitHub (), mens .
ViennaNET.Sagas
Når et projekt skifter til DDD og mikroservices-arkitektur, opstår der problemer i forbindelse med implementeringen af mekanismer til distribuerede transaktioner, fordi mange scenarier ofte involverer flere domæner. Sådanne mekanismer kan du lære mere om, f.eks. .
I vores projekter har vi implementeret en simpel, men nyttig mekanisme: en saga, nærmere bestemt en saga baseret på orkestrering. Ideén er som følger: der er et forretningsscenarie, hvor det er nødvendigt at udføre operationer i forskellige tjenester i rækkefølge; i tilfælde af problemer på noget trin skal der kaldes en tilbagesendelsesprocedure for alle tidligere trin, hvor den er planlagt. Således, ved afslutningen af sagaen, uanset succesen, får vi konsistente data på tværs af alle domæner.
Vores implementering er indtil videre lavet i en grundlæggende form og er ikke bundet til brug af nogen metoder til interaktion med andre tjenester. Det er ikke svært at anvende: blot opret en afledt klasse fra den basis abstrakte klasse SagaBase, hvor T er din kontekstklasse, som kan opbevare de originale data, der er nødvendige for at arbejde med sagaen, samt nogle mellemliggende resultater. En instans af konteksten vil blive sendt til alle trin under udførelsen. Selve sagaen er en stateless klasse, så instansen kan placeres i DI som en Singleton for at få de nødvendige afhængigheder.
Eksempel på erklæring:
public class ExampleSaga : SagaBase
{
public ExampleSaga()
{
Step("Step 1")
.WithAction(c => ...)
.WithCompensation(c => ...);
AsyncStep("Step 2")
.WithAction(async c => ...);
}
}
Eksempel på kald:
var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);
Fuldstændige eksempler på forskellige implementeringer kan ses og i samlingen med .
ViennaNET.Orm.*
Biblioteksættet til arbejde med forskellige databaser via Nhibernate. Vi anvender en DB-First tilgang med Liquibase, så her er der kun funktionalitet til at arbejde med data i den færdige database.
ViennaNET.Orm.Seedwork og ViennaNET.Orm er de vigtigste assemblyer, der indeholder de grundlæggende grænseflader og deres implementeringer. Lad os se nærmere på deres indhold.
Interface IEntityFactoryService og dens implementering EntityFactoryService er det primære udgangspunkt for arbejde med databasen, da her oprettes Unit of Work, repositorier til arbejde med specifikke enheder samt eksekveringsenheder til kommandoer og direkte SQL-forespørgsler. Nogle gange kan det være praktisk at begrænse klassens muligheder for at arbejde med databasen, for eksempel kun give mulighed for at læse data. I sådanne tilfælde har IEntityFactoryService en forfader – grænsefladen IEntityRepositoryFactory, hvor der kun er erklæret en metode til oprettelse af repositorier.
Til direkte adgang til databasen anvendes mekanismen med udbydere. For hver databasesystem, vi bruger i vores teams, er der en specifik implementering: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.
I én applikation kan der samtidig være registreret flere udbydere, hvilket gør det muligt, for eksempel at udføre en trinvis migration fra et databasesystem til et andet inden for én tjeneste uden ekstra infrastrukturomkostninger. Mekanismen til at vælge den nødvendige forbindelse og dermed udbyderen for en specifik klasse-enhed (for hvilken tabellerne i databasen mappes) er implementeret gennem registreringen af enheden i klassen BoundedContext (indeholder metoden til at registrere domænenheder) eller dens underklasse ApplicationContext (indeholder metoder til at registrere applikationsenheder, direkte forespørgsler og kommandoer), hvor identifikatoren for forbindelsen fra konfigurationen tages som argument:
"db": [
{
"nick": "mssql_connection",
"dbServerType": "MSSQL",
"ConnectionString": "...",
"useCallContext": true
},
{
"nick": "oracle_connection",
"dbServerType": "Oracle",
"ConnectionString": "..."
}
],
Eksempel på ApplicationContext:
internal sealed class DbContext : ApplicationContext
{
public DbContext()
{
AddEntity("mssql_connection");
AddEntity("oracle_connection");
AddEntity("oracle_connection");
}
}
Hvis forbindelsesidentifikatoren ikke er angivet, vil forbindelsen med navnet «default» blive anvendt.
Mapping af enheder til databaser sker ved hjælp af standardmetoder fra NHibernate. Man kan bruge XML-filer eller klasser til at beskrive dette. For nemt at skrive stub-repositorier i unit-tests findes der et bibliotek ViennaNET.TestUtils.Orm.
Fuldstændige eksempler på brug af ViennaNET.Orm.* kan findes .
ViennaNET.Messaging.*
Et sæt biblioteker til arbejde med køer.
Det samme tilgang som med forskellige DBMS'er er blevet valgt til arbejdet med køer, nemlig en maksimalt ensartet tilgang med henblik på at arbejde med biblioteket, uanset hvilken kømanager der anvendes. Biblioteket ViennaNET.Messaging ansvarer netop for denne ensartethed, og ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue og ViennaNET.Messaging.KafkaQueue indeholder implementeringer af adaptere til henholdsvis IBM MQ, RabbitMQ og Kafka.
Der er to processer ved arbejde med køer: modtagelse af beskeder og afsendelse.
Lad os se på modtagelse. Der er her to muligheder: til konstant lytning og til modtagelse af en enkelt besked. For konstant lytning af en kø er det nødvendigt først at beskrive en processor-klasse, der er arvet fra IMessageProcessor, som vil være ansvarlig for behandling af den indkommende besked. Derefter skal den 'bindes' til en bestemt kø, hvilket gøres ved registrering i IQueueReactorFactory med angivelse af køens identifikator fra konfigurationen:
"messaging": {
"ApplicationName": "MyApplication"
},
"rabbitmq": {
"queues": [
{
"id": "myQueue",
"queuename": "lalala",
...
}
]
},
Eksempel på opstart af lytning:
_queueReactorFactory.Register<MyMessageProcessor>("myQueue");
var queueReactor = queueReactorFactory.CreateQueueReactor("myQueue");
queueReactor.StartProcessing();
Når tjenesten starter, og metoden til at begynde lytning kaldes, vil alle beskeder fra den angivne kø blive sendt til den relevante processor.
For at modtage en enkelt besked i fabrik-interface IMessagingComponentFactory er der en metode CreateMessageReceiver, der vil oprette en modtager, som venter på beskeder fra den angivne kø:
using (var receiver = _messagingComponentFactory.CreateMessageReceiver<TestMessage>("myQueue"))
{
var message = receiver.Receive();
}
For at sende en besked skal man bruge samme metode IMessagingComponentFactory og oprette en beskedafsender:
using (var sender = _messagingComponentFactory.CreateMessageSender<MyMessage>("myQueue"))
{
sender.SendMessage(new MyMessage { Value = ...});
}
Til serialisering og deserialisering af meddelelser er der tre klare muligheder: almindelig tekst, XML og JSON, men hvis nødvendigt, kan man roligt lave sine egne implementeringer af grænsefladerne IMessageSerializer og IMessageDeserializer.
Vi har forsøgt at bevare de unikke funktioner for hver kømanager, for eksempel ViennaNET.Messaging.MQSeriesQueue giver mulighed for at sende ikke kun tekstbeskeder, men også bytebeskeder, og ViennaNET.Messaging.RabbitMQQueue understøtter routing og oprettelse af kører 'on the fly'. I vores adapterwrapper til RabbitMQ er der også implementeret en slags RPC: vi sender en meddelelse og venter på svar fra en speciel midlertidig kø, som kun oprettes til én svarmeddelelse.
Her er .
ViennaNET.CallContext
Vi bruger køer ikke kun til integration mellem forskellige systemer, men også til kommunikation mellem mikrotjenester i én applikation, for eksempel inden for en saga. Dette har medført behovet for at overføre supplerende data sammen med meddelelsen, såsom brugernavn, anmodnings-id til end-to-end logging, ip-adresse på kilden og autorisationsoplysninger. For at implementere overførslen af disse data har vi udviklet et bibliotek ViennaNET.CallContext, som gør det muligt at gemme data fra den indgående serviceforespørgsel. På den måde er det ligegyldigt, hvordan anmodningen blev foretaget, enten via kø eller via Http. Derefter, før den udgående anmodning eller meddelelse sendes, hentes dataene fra konteksten og placeres i overskrifterne. På den måde får den næste tjeneste de supplerende data og kan agere på samme måde.
Tak for opmærksomheden, vi ser frem til jeres kommentarer og pull requests!
Kilde: habr.com
