ViennaNET: een set bibliotheken voor de backend. Deel 2

De Raiffeisenbank .NET-ontwikkelaarsgemeenschap blijft de inhoud van ViennaNET kort beoordelen. Over hoe en waarom we hiertoe zijn gekomen, je kunt het eerste deel lezen.

In dit artikel zullen we nog te overwegen bibliotheken doornemen voor het werken met gedistribueerde transacties, wachtrijen en databases, die te vinden zijn in onze GitHub-repository (bronnen zijn hier), en Nuget-pakketten hier.

ViennaNET: een set bibliotheken voor de backend. Deel 2

WenenNET.Sagas

Wanneer een project overschakelt naar DDD en microservice-architectuur, ontstaat er, wanneer de bedrijfslogica over verschillende services wordt gedistribueerd, een probleem met betrekking tot de noodzaak om een ​​gedistribueerd transactiemechanisme te implementeren, omdat veel scenario's vaak meerdere domeinen tegelijk beïnvloeden. U kunt meer in detail kennis maken met dergelijke mechanismen, bijvoorbeeld in het boek "Microservices Patterns", Chris Richardson.

In onze projecten hebben we een eenvoudig maar nuttig mechanisme geïmplementeerd: een saga, of beter gezegd een op orkestratie gebaseerde saga. De essentie ervan is als volgt: er is een bepaald bedrijfsscenario waarin het nodig is om opeenvolgend bewerkingen in verschillende services uit te voeren, en als er bij welke stap dan ook problemen optreden, is het noodzakelijk om de terugdraaiprocedure voor alle voorgaande stappen aan te roepen, waarbij dit het geval is mits. Dus aan het einde van de saga ontvangen we, ongeacht het succes, consistente gegevens op alle domeinen.

Onze implementatie gebeurt nog steeds in de basisvorm en is niet gebonden aan het gebruik van enige interactiemethode met andere diensten. Het is niet moeilijk om te gebruiken: maak gewoon een afstammeling van de abstracte basisklasse SagaBase<T>, waarbij T uw contextklasse is waarin u de initiële gegevens kunt opslaan die nodig zijn om de saga te laten werken, evenals enkele tussenresultaten. De contextinstantie wordt tijdens de uitvoering aan alle stappen doorgegeven. Saga zelf is een staatloze klasse, dus de instantie kan als een Singleton in DI worden geplaatst om de nodige afhankelijkheden te krijgen.

Voorbeeldadvertentie:

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

Voorbeeld oproep:

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

Volledige voorbeelden van verschillende implementaties kunnen worden bekeken hier en in montage met testen.

WenenNET.Orm.*

Een reeks bibliotheken voor het werken met verschillende databases via Nhibernate. Wij hanteren de DB-First aanpak met behulp van Liquibase, waardoor er alleen functionaliteit is voor het werken met data in een kant-en-klare database.

ViennaNET.Orm.Seedwork и ViennaNET.Orm – hoofdassemblages die respectievelijk basisinterfaces en hun implementaties bevatten. Laten we hun inhoud in meer detail bekijken.

interface IEntityFactoryService en de implementatie ervan EntityFactoryService zijn het belangrijkste startpunt voor het werken met de database, aangezien hier de Unit of Work, opslagplaatsen voor het werken met specifieke entiteiten, evenals uitvoerders van opdrachten en directe SQL-query's worden gemaakt. Soms is het handig om de mogelijkheden van een klasse voor het werken met een database te beperken, bijvoorbeeld om de mogelijkheid te bieden alleen gegevens te lezen. Voor dergelijke gevallen IEntityFactoryService er is een voorouderinterface IEntityRepositoryFactory, dat alleen een methode declareert voor het maken van repository's.

Om rechtstreeks toegang te krijgen tot de database wordt het providermechanisme gebruikt. Elk DBMS dat we in onze teams gebruiken, heeft zijn eigen implementatie: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Tegelijkertijd kunnen meerdere aanbieders tegelijkertijd in één applicatie worden geregistreerd, waardoor bijvoorbeeld in het kader van één dienst, zonder kosten voor het aanpassen van de infrastructuur, een stapsgewijze migratie kan worden uitgevoerd van het ene DBMS naar het andere. Het mechanisme voor het selecteren van de vereiste verbinding en dus de provider voor een specifieke entiteitsklasse (waarvoor mapping naar databasetabellen is geschreven) wordt geïmplementeerd door de entiteit te registreren in de klasse BoundedContext (bevat een methode voor het registreren van domeinentiteiten) of de opvolger ervan ApplicationContext (bevat methoden voor het registreren van applicatie-entiteiten, directe verzoeken en opdrachten), waarbij de verbindings-ID uit de configuratie als argument wordt geaccepteerd:

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

Voorbeeld toepassingscontext:

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

Als het verbindings-ID niet is opgegeven, wordt de verbinding met de naam “standaard” gebruikt.

Het direct toewijzen van entiteiten aan databasetabellen wordt geïmplementeerd met behulp van standaard NHibernate-tools. U kunt de beschrijving zowel via XML-bestanden als via klassen gebruiken. Voor het gemakkelijk schrijven van stub-repository's in Unit-tests is er een bibliotheek ViennaNET.TestUtils.Orm.

Volledige voorbeelden van het gebruik van ViennaNET.Orm.* zijn te vinden hier.

ViennaNET.Messaging.*

Een set bibliotheken voor het werken met wachtrijen.

Voor het werken met wachtrijen is gekozen voor dezelfde aanpak als bij verschillende DBMS'en, namelijk de maximaal mogelijke uniforme aanpak wat betreft het werken met de bibliotheek, ongeacht de gebruikte wachtrijmanager. Bibliotheek ViennaNET.Messaging is precies verantwoordelijk voor deze eenwording, en ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue и ViennaNET.Messaging.KafkaQueue bevatten adapterimplementaties voor respectievelijk IBM MQ, RabbitMQ en Kafka.

Bij het werken met wachtrijen zijn er twee processen: een bericht ontvangen en verzenden.

Overweeg om te ontvangen. Er zijn hier 2 opties: voor continu luisteren en voor het ontvangen van een enkel bericht. Om voortdurend naar de wachtrij te luisteren, moet u eerst de processorklasse beschrijven die is geërfd IMessageProcessor, die verantwoordelijk is voor de verwerking van het binnenkomende bericht. Vervolgens moet deze worden “gekoppeld” aan een specifieke wachtrij; dit gebeurt via registratie in IQueueReactorFactory geeft de wachtrij-ID uit de configuratie aan:

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

Voorbeeld van beginnen met luisteren:

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

Wanneer de service vervolgens start en de methode wordt aangeroepen om te beginnen met luisteren, gaan alle berichten uit de opgegeven wachtrij naar de overeenkomstige processor.

Om één enkel bericht te ontvangen in een fabrieksinterface IMessagingComponentFactory er is een methode CreateMessageReceiverwaarmee een ontvanger wordt aangemaakt die wacht op een bericht uit de opgegeven wachtrij:

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

Om een ​​bericht te sturen je moet hetzelfde gebruiken IMessagingComponentFactory en maak een afzender van het bericht aan:

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

Er zijn drie kant-en-klare opties voor het serialiseren en deserialiseren van een bericht: alleen tekst, XML en JSON, maar indien nodig kunt u eenvoudig uw eigen interface-implementaties maken IMessageSerializer и IMessageDeserializer.

We hebben geprobeerd de unieke mogelijkheden van elke wachtrijbeheerder te behouden, b.v. ViennaNET.Messaging.MQSeriesQueue Hiermee kunt u niet alleen tekst-, maar ook byte-berichten verzenden, en ViennaNET.Messaging.RabbitMQQueue ondersteunt routing en on-the-fly wachtrijen. Onze adapterwrapper voor RabbitMQ implementeert ook enige schijn van RPC: we sturen een bericht en wachten op een antwoord van een speciale tijdelijke wachtrij, die slechts voor één antwoordbericht wordt gemaakt.

Hier een voorbeeld van het gebruik van wachtrijen met basisverbindingsnuances.

ViennaNET.CallContext

We gebruiken wachtrijen niet alleen voor integratie tussen verschillende systemen, maar ook voor communicatie tussen microservices van dezelfde applicatie, bijvoorbeeld binnen een saga. Dit leidde tot de noodzaak om samen met het bericht aanvullende gegevens te verzenden, zoals de login van de gebruiker, de verzoekidentificatie voor end-to-end loggen, het bron-IP-adres en autorisatiegegevens. Om het doorsturen van deze gegevens te implementeren, hebben we een bibliotheek ontwikkeld ViennaNET.CallContext, waarmee u gegevens kunt opslaan van een verzoek dat de service binnenkomt. In dit geval maakt het niet uit hoe het verzoek is gedaan, via een wachtrij of via Http. Voordat het uitgaande verzoek of bericht wordt verzonden, worden vervolgens gegevens uit de context gehaald en in de headers geplaatst. De volgende service ontvangt dus de hulpgegevens en beheert deze op dezelfde manier.

Bedankt voor uw aandacht, we kijken uit naar uw opmerkingen en pull-aanvragen!

Bron: www.habr.com

Voeg een reactie