ViennaNET: en uppsättning bibliotek för backend. Del 2

Raiffeisenbank .NET-utvecklargemenskapen fortsätter att kort granska innehållet i ViennaNET. Om hur och varför vi kom fram till detta, du kan läsa den första delen.

I den här artikeln kommer vi att gå igenom ännu inte övervägda bibliotek för att arbeta med distribuerade transaktioner, köer och databaser, som finns i vårt GitHub-förråd (källor finns här) och Nuget-paket här.

ViennaNET: en uppsättning bibliotek för backend. Del 2

ViennaNET.Sagas

När ett projekt går över till DDD och mikrotjänstarkitektur, då när affärslogik distribueras över olika tjänster, uppstår ett problem relaterat till behovet av att implementera en distribuerad transaktionsmekanism, eftersom många scenarier ofta påverkar flera domäner samtidigt. Du kan bekanta dig med sådana mekanismer mer i detalj, till exempel, i boken "Microservices Patterns", Chris Richardson.

I våra projekt har vi implementerat en enkel men användbar mekanism: en saga, eller snarare en orkestreringsbaserad saga. Dess kärna är följande: det finns ett visst affärsscenario där det är nödvändigt att sekventiellt utföra operationer i olika tjänster, och om några problem uppstår vid något steg är det nödvändigt att anropa återställningsproceduren för alla tidigare steg, där det är försedd. Således, i slutet av sagan, oavsett framgång, får vi konsekventa data inom alla domäner.

Vår implementering görs fortfarande i sin grundläggande form och är inte bunden till användningen av några metoder för interaktion med andra tjänster. Det är inte svårt att använda: gör bara en ättling till den abstrakta basklassen SagaBase, där T är din kontextklass där du kan lagra den initiala data som behövs för att sagan ska fungera, samt några mellanliggande resultat. Kontextinstansen kommer att skickas igenom till alla steg under körningen. Saga i sig är en statslös klass, så instansen kan placeras i DI som en Singleton för att få de nödvändiga beroenden.

Exempelannons:

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

Exempelsamtal:

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

Fullständiga exempel på olika implementeringar kan ses här och i montering med tester.

ViennaNET.Orm.*

En uppsättning bibliotek för att arbeta med olika databaser via Nhibernate. Vi använder DB-First-metoden med Liquibase, så det finns bara funktionalitet för att arbeta med data i en färdig databas.

ViennaNET.Orm.Seedwork и ViennaNET.Orm – huvudenheter som innehåller grundläggande gränssnitt och deras implementeringar. Låt oss titta på deras innehåll mer i detalj.

gränssnitt IEntityFactoryService och dess genomförande EntityFactoryService är den huvudsakliga utgångspunkten för att arbeta med databasen, eftersom Unit of Work, repositories för att arbeta med specifika entiteter, såväl som executorer av kommandon och direkta SQL-frågor skapas här. Ibland är det bekvämt att begränsa möjligheterna för en klass för att arbeta med en databas, till exempel för att ge möjligheten att bara läsa data. För sådana fall IEntityFactoryService det finns ett förfader - gränssnitt IEntityRepositoryFactory, som bara deklarerar en metod för att skapa arkiv.

För att få direkt åtkomst till databasen används leverantörsmekanismen. Varje DBMS vi använder i våra team har sin egen implementering: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Samtidigt kan flera leverantörer registreras i en applikation samtidigt, vilket gör det möjligt att till exempel inom ramen för en tjänst, utan några kostnader för att modifiera infrastrukturen, genomföra en steg-för-steg-migrering fr.o.m. ett DBMS till ett annat. Mekanismen för att välja den erforderliga anslutningen och därför leverantören för en specifik entitetsklass (för vilken mappning till databastabeller skrivs) implementeras genom att registrera entiteten i klassen BoundedContext (innehåller en metod för att registrera domänentiteter) eller dess efterföljare ApplicationContext (innehåller metoder för att registrera applikationsenheter, direkta förfrågningar och kommandon), där anslutningsidentifieraren från konfigurationen accepteras som ett argument:

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

Exempel ApplicationContext:

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

Om anslutnings-ID inte anges kommer anslutningen med namnet "default" att användas.

Direktmappning av entiteter till databastabeller implementeras med hjälp av standardverktyg för NHibernate. Du kan använda beskrivningen både genom xml-filer och genom klasser. För bekväm skrivning av stubbförråd i enhetstester finns ett bibliotek ViennaNET.TestUtils.Orm.

Fullständiga exempel på användning av ViennaNET.Orm.* finns här.

ViennaNET.Messaging.*

En uppsättning bibliotek för att arbeta med köer.

För att arbeta med köer valdes samma tillvägagångssätt som med olika DBMS, nämligen maximalt möjligt enhetligt tillvägagångssätt vad gäller arbete med biblioteket, oavsett vilken köhanterare som används. Bibliotek ViennaNET.Messaging är just ansvarig för denna enande, och ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue и ViennaNET.Messaging.KafkaQueue innehåller adapterimplementationer för IBM MQ, RabbitMQ respektive Kafka.

När man arbetar med köer finns det två processer: ta emot ett meddelande och skicka det.

Överväg att ta emot. Det finns två alternativ här: för kontinuerlig lyssning och för att ta emot ett enda meddelande. För att hela tiden lyssna på kön måste du först beskriva processorklassen som ärvts från IMessageProcessor, som kommer att ansvara för att bearbeta det inkommande meddelandet. Därefter måste den vara "länkad" till en specifik kö, detta görs genom registrering i IQueueReactorFactory anger köidentifieraren från konfigurationen:

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

Exempel på att börja lyssna:

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

Sedan, när tjänsten startar och metoden anropas för att börja lyssna, kommer alla meddelanden från den angivna kön att gå till motsvarande processor.

För att ta emot ett enda meddelande i ett fabriksgränssnitt IMessagingComponentFactory det finns en metod CreateMessageReceivervilket kommer att skapa en mottagare som väntar på ett meddelande från den kön som anges för den:

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

För att skicka ett meddelande du måste använda samma IMessagingComponentFactory och skapa en meddelandeavsändare:

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

Det finns tre färdiga alternativ för att serialisera och deserialisera ett meddelande: bara text, XML och JSON, men vid behov kan du enkelt göra dina egna gränssnittsimplementeringar IMessageSerializer и IMessageDeserializer.

Vi har försökt bevara varje köhanterares unika möjligheter, t.ex. ViennaNET.Messaging.MQSeriesQueue låter dig skicka inte bara text utan även bytemeddelanden och ViennaNET.Messaging.RabbitMQQueue stöder routing och on-the-fly köer. Vår adapteromslag för RabbitMQ implementerar också en viss sken av RPC: vi skickar ett meddelande och väntar på ett svar från en speciell tillfällig kö, som bara skapas för ett svarsmeddelande.

Här ett exempel på att använda köer med grundläggande anslutningsnyanser.

ViennaNET.CallContext

Vi använder köer inte bara för integration mellan olika system, utan även för kommunikation mellan mikrotjänster av samma applikation, till exempel inom en saga. Detta ledde till behovet av att tillsammans med meddelandet överföra sådan extra data som användarinloggning, begärandeidentifierare för slut-till-ände-loggning, källans IP-adress och behörighetsdata. För att genomföra vidarebefordran av dessa data utvecklade vi ett bibliotek ViennaNET.CallContext, som låter dig lagra data från en begäran som går in i tjänsten. I det här fallet spelar ingen roll hur begäran gjordes, via en kö eller via Http. Sedan, innan den utgående begäran eller meddelandet skickas, tas data från sammanhanget och placeras i rubrikerna. Följaktligen tar nästa tjänst emot extradata och hanterar den på samma sätt.

Tack för din uppmärksamhet, vi ser fram emot dina kommentarer och förfrågningar!

Källa: will.com

Lägg en kommentar