ViennaNET: en samling av bibliotek för backend. Del 2

Gemenskapen av .NET-utvecklare pÄ Raiffeisenbank fortsÀtter den korta genomgÄngen av innehÄllet i ViennaNET. Om hur och varför vi kom hit, kan du lÀsa i första delen.

I denna artikel kommer vi att gÄ igenom de bibliotek som Ànnu inte har diskuterats för att arbeta med distribuerade transaktioner, köer och databaser, som du kan hitta i vÄrt GitHub-repo (kÀllkoden finns hÀr), och Nuget-paketen finns hÀr.

ViennaNET: en samling av bibliotek för backend. Del 2

ViennaNET.Sagas

NÀr ett projekt övergÄr till DDD och mikrotjÀnstarkitektur, uppstÄr en frÄga gÀllande implementeringen av mekanismen för distribuerade transaktioner, eftersom mÄnga scenarier ofta berör flera domÀner. Mer om sÄdana mekanismer kan du lÀra dig, till exempel, i boken "Microservices Patterns", Chris Richardson.

I vÄra projekt har vi implementerat en enkel, men anvÀndbar mekanism: en saga, eller mer specifikt en saga baserad pÄ orkestrering. Dess syfte Àr följande: det finns ett visst affÀrsscenario dÀr det Àr nödvÀndigt att successivt utföra operationer i olika tjÀnster, och om det uppstÄr nÄgra problem vid nÄgot steg, mÄste en rollback-procedur för alla föregÄende steg som det Àr avsett för anropas. SÄledes, i slutet av sagans utförande, oavsett om det har varit framgÄngsrikt eller inte, fÄr vi konsekventa data i alla domÀner.

VÄr implementering Àr hittills gjord i grundlÀggande form och Àr inte knuten till anvÀndningen av nÄgra metoder för interaktion med andra tjÀnster. Att anvÀnda den Àr enkelt: det rÀcker att skapa en arvsklass frÄn den grundlÀggande abstrakta klassen SagaBase, dÀr T Àr din kontextklass, i vilken du kan lagra ursprungliga data nödvÀndiga för att arbeta med sagan samt nÄgra mellanresultat. En instans av kontexten kommer att matas vidare i alla steg under körningen. SjÀlva sagan Àr en stateless klass, sÄ instansen kan registreras i DI som Singleton för att fÄ nödvÀndiga beroenden.

Exempel pÄ deklaration:

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

Exempel pÄ anrop:

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

FullstÀndiga exempel pÄ olika implementeringar kan ses hÀr och i paket med tester.

ViennaNET.Orm.*

Ett bibliotekspaket för att arbeta med olika databaser via Nhibernate. Vi anvÀnder en DB-First-ansats med Liquibase, sÄ hÀr finns endast funktionalitet för att arbeta med data i en fÀrdig databas.

ViennaNET.Orm.Seedwork och ViennaNET.Orm Àr huvudpaketen som innehÄller grundlÀggande grÀnssnitt och deras implementationer. LÄt oss titta nÀrmare pÄ deras innehÄll.

GrĂ€nssnitt IEntityFactoryService och dess implementation EntityFactoryService Ă€r den huvudsakliga startpunkten för att arbeta med databasen, eftersom hĂ€r skapas Unit of Work, repositorier för att arbeta med specifika entiteter, samt kommandoförare och direkta SQL-frĂ„gor. Ibland Ă€r det praktiskt att begrĂ€nsa klassens möjligheter att arbeta med databasen, till exempel att enbart ge möjlighet att lĂ€sa data. För sĂ„dana fall har IEntityFactoryService en förĂ€lder – grĂ€nssnittet IEntityRepositoryFactory, dĂ€r endast metoden för att skapa repositorier deklareras.

För direkt Ätkomst till databasen anvÀnds ett leverantörsmekanism. För varje DBMS som vi anvÀnder i vÄra team finns det en specifik implementation: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Samtidigt kan flera leverantörer registreras i en applikation, vilket möjliggör att man inom en och samma tjÀnst utan extra infrastrukturkostnader kan genomföra en stegvis migration frÄn en DBMS till en annan. Mekanismen för att vÀlja rÀtt anslutning och dÀrmed leverantör för en specifik klass-entity (för vilken mappning till databastabeller skrivs) implementeras genom registrering av en entitet i klassen BoundedContext (innehÄller metod för registrering av domÀnentiteter) eller dess arvtagare ApplicationContext (innehÄller metoder för registrering av applikationsentiteter, direkta frÄgor och kommandon), dÀr anslutningsidentifikatorn frÄn konfigurationen tas som argument:

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

Exempel pÄ ApplicationContext:

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

Om anslutningsidentifikatorn inte anges anvÀnds anslutningen med namnet «default».

Direkt mappning av entiteter till databas-tabeller implementeras med standardverktyg i NHibernate. Man kan anvÀnda beskrivningar antingen genom xml-filer eller genom klasser. För att underlÀtta för skrivandet av stubbrepositorier i enhetstester finns det ett bibliotek ViennaNET.TestUtils.Orm.

FullstÀndiga exempel pÄ anvÀndning av ViennaNET.Orm.* kan hittas hÀr.

ViennaNET.Messaging.*

Ett bibliotek för att arbeta med köer.

För att arbeta med köer valdes samma angreppssÀtt som för olika databashanterare, nÀmligen ett maximalt enhetligt tillvÀgagÄngssÀtt nÀr det gÀller arbete med biblioteket, oavsett vilken köhanterare som anvÀnds. Biblioteket ViennaNET.Messaging tjÀnar just till denna enhetlighet, och ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue och ViennaNET.Messaging.KafkaQueue innehÄller implementeringar av adaptrar för IBM MQ, RabbitMQ och Kafka respektive.

I arbetet med köer finns det tvÄ processer: mottagning av meddelande och utskick.

LÄt oss titta pÄ mottagningen. HÀr finns det 2 alternativ: för konstant lyssning och för mottagning av ett enstaka meddelande. För konstant lyssning pÄ kön mÄste man först beskriva en processor-klass som Àrver frÄn IMessageProcessor, som kommer att ansvara för behandlingen av det inkommande meddelandet. DÀrefter mÄste den "kopplas" till en viss kö, vilket görs genom registrering i IQueueReactorFactory med angivande av köidentifieraren frÄn konfigurationen:

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

Exempel pÄ start av lyssning:

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

Sedan, nÀr tjÀnsten startar och metoden för att börja lyssna anropas, kommer alla meddelanden frÄn den angivna kön att nÄ den motsvarande processorn.

För att ta emot ett enstaka meddelande i fabriksgrÀnssnittet IMessagingComponentFactory finns det en metod CreateMessageReceiver, som skapar en mottagare som vÀntar pÄ meddelanden frÄn den angivna kön:

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

För att skicka ett meddelande mÄste man anvÀnda samma IMessagingComponentFactory och skapa en meddelandeavsÀndare:

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

För serialisering och deserialisering av meddelanden finns det tre fÀrdiga alternativ: ren text, XML och JSON, men vid behov kan man enkelt göra sina egna implementationer av grÀnssnitten. IMessageSerializer och IMessageDeserializer.

Vi har försökt bevara de unika möjligheterna hos varje köhanterare, till exempel ViennaNET.Messaging.MQSeriesQueue kan skicka bÄde text- och byte-meddelanden, medan ViennaNET.Messaging.RabbitMQQueue stöder routing och skapande av köer "on-the-fly". I vÄr wrapper-adapter för RabbitMQ finns ocksÄ en form av RPC: vi skickar ett meddelande och vÀntar pÄ svar frÄn en speciell temporÀr kö som skapas enbart för ett svarsmeddelande.

HÀr Àr ett exempel pÄ anvÀndning av köer med viktiga anslutningsfinesser.

ViennaNET.CallContext

Vi anvÀnder köer inte bara för integration mellan olika system, utan Àven för kommunikation mellan mikrotjÀnster inom en och samma applikation, till exempel inom ramen för en saga. Detta har lett till behovet av att överföra sÄdana hjÀlpdata tillsammans med meddelandet som anvÀndarnamn, begÀrnings-ID för spÄrning av loggar, kÀllans IP-adress och autentiseringsinformation. För att realisera överföringen av dessa data har vi utvecklat ett bibliotek ViennaNET.CallContext, vilket gör det möjligt att lagra data frÄn ingÄende begÀrningar till tjÀnsten. Hur begÀrningen gjordes, via kö eller via Http, spelar ingen roll. Sedan, innan avsÀndandet av en utgÄende begÀran eller meddelande, hÀmtas data frÄn kontexten och placeras i rubriker. PÄ sÄ sÀtt fÄr nÀsta tjÀnst hjÀlpdata och hanterar dem pÄ liknande sÀtt.

Tack för er uppmÀrksamhet, vi ser fram emot era kommentarer och pull requests!

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster