ViennaNET: een set bibliotheken voor de backend. Deel 2

De .NET-ontwikkelaarsgemeenschap van Raiffeisenbank blijft een korte analyse geven van de inhoud van ViennaNET. Waarom en hoe we hier terecht zijn gekomen, is te lezen in het eerste deel.

In dit artikel bespreken we nog niet behandelde bibliotheken voor het werken met gedistribueerde transacties, wachtrijen en databases die te vinden zijn in onze repository op GitHub (de bronteksten bevinden zich hier), terwijl Nuget-pakketten hier.

ViennaNET: een set bibliotheken voor de backend. Deel 2

ViennaNET.Sagas

Wanneer een project overschakelt op DDD en microservices-architectuur, ontstaat er bij het scheiden van de bedrijfslogica over verschillende diensten een probleem met de noodzaak om een mechanisme voor gedistribueerde transacties te implementeren, aangezien veel scenario's vaak meerdere domeinen raken. Dergelijke mechanismen kunnen bijvoorbeeld beter worden begrepen in het boek ‘Microservices Patterns’, Chris Richardson.

In onze projecten hebben we een eenvoudig maar nuttig mechanisme geïmplementeerd: een saga, meer specifiek een saga op basis van orchestratie. Het idee is als volgt: er is een bepaalde bedrijfs scenario waarin opeenvolgend operaties in verschillende services moeten worden uitgevoerd, waarbij, bij problemen op een van de stappen, het noodzakelijk is om een terugrolprocedure te activeren voor alle eerdere stappen, waar van toepassing. Op deze manier hebben we aan het einde van de uitvoering van de saga, ongeacht de uitkomst, consistente gegevens in alle domeinen.

Onze implementatie is voorlopig in basisvorm en is niet afhankelijk van interactiemethoden met andere services. Toepassen is eenvoudig: je hoeft alleen maar een afgeleide klasse van de abstracte klasse SagaBase te maken, waarbij T jouw contextklasse is, waarin je de originele gegevens kunt opslaan die nodig zijn voor het werken met de saga, evenals enkele tussenresultaten. Een instantie van de context wordt doorgestuurd naar alle stappen tijdens de uitvoering. De saga zelf is een stateless klasse, zodat de instantie als Singleton in DI kan worden geplaatst om de nodige afhankelijkheden te verkrijgen.

Voorbeeld van declaratie:

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

Voorbeeld van aanroep:

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

Volledige voorbeelden van verschillende implementaties zijn te bekijken hier en in de pakket met tests.

ViennaNET.Orm.*

Een set bibliotheken voor het werken met verschillende databases via Nhibernate. We gebruiken de DB-First benadering met Liquibase, daarom is er hier alleen functionaliteit voor het werken met gegevens in een bestaande database.

ViennaNET.Orm.Seedwork en ViennaNET.Orm – zijn de belangrijkste assemblies die de basisinterfaces en hun implementaties bevatten. Laten we dieper ingaan op hun inhoud.

Interface IEntityFactoryService en zijn implementatie EntityFactoryService is het belangrijkste startpunt voor het werken met de database, aangezien hier de Unit of Work, de repositories voor het werken met specifieke entiteiten, en de uitvoerders van commando's en directe SQL-query's worden aangemaakt. Soms is het handig om de mogelijkheden van de klasse voor database-interactie te beperken, bijvoorbeeld door alleen leesrechten te geven voor gegevens. Voor dergelijke gevallen heeft IEntityFactoryService een precedent – de interface IEntityRepositoryFactory, waarin alleen de methode voor het aanmaken van repositories is gedeclareerd.

Voor directe toegang tot de database wordt het providersmechanisme gebruikt. Voor elke gebruikte DBMS in onze teams is er een eigen implementatie: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Tegelijkertijd kunnen er meerdere providers in één applicatie zijn geregistreerd, wat het mogelijk maakt om bijvoorbeeld binnen één service stapsgewijze migraties van de ene DBMS naar de andere uit te voeren zonder extra kosten voor infrastructuuraanpassingen. Het mechanisme voor het kiezen van de benodigde verbinding en dus de provider voor een specifieke klasse-entiteit (waarvoor de mapping op de database tabellen wordt geschreven) is geïmplementeerd via het registreren van de entiteit in de klasse BoundedContext (bevat een methode voor het registreren van domeinentiteiten) of de afgeleide klasse ApplicationContext (bevat methoden voor het registreren van applicatie-entiteiten, directe verzoeken en commando's), waar bij het argument de verbindingsidentificatie uit de configuratie wordt genomen:

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

Voorbeeld van ApplicationContext:

internal sealed class DbContext : ApplicationContext
{
  public DbContext()
  {
    AddEntity("mssql_connection");
    AddEntity("oracle_connection");
    AddEntity("oracle_connection");
  }
}

Als de verbindingsidentificatie niet is opgegeven, wordt de verbinding met de naam "default" gebruikt.

De mapping van entiteiten naar databaseschema's wordt uitgevoerd met de standaardmogelijkheden van NHibernate. Beschrijvingen kunnen zowel via xml-bestanden als via klassen worden gebruikt. Voor het gemakkelijk schrijven van stub-repositories 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 dezelfde aanpak gekozen als voor verschillende databasesystemen, namelijk een zo uniform mogelijke aanpak vanuit het perspectief van werken met de bibliotheek, ongeacht de gebruikte wachtrijenmanager. De bibliotheek ViennaNET.Messaging is verantwoordelijk voor deze uniformiteit, en ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue en ViennaNET.Messaging.KafkaQueue bevatten implementaties van adapters voor respectievelijk IBM MQ, RabbitMQ en Kafka.

Bij het werken met wachtrijen zijn er twee processen: het ontvangen van berichten en het verzenden.

Laten we kijken naar het ontvangen. Hier zijn er 2 opties: voor continue monitoring en voor het ontvangen van een enkel bericht. Voor continue monitoring van de wachtrij moet eerst een processor-klasse worden beschreven, die is afgeleid van IMessageProcessor, die verantwoordelijk zal zijn voor de verwerking van het inkomende bericht. Vervolgens moet deze "gebonden" worden aan een bepaalde wachtrij, dit gebeurt door registratie in IQueueReactorFactory met vermelding van de identifier van de wachtrij uit de configuratie:

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

Voorbeeld van het starten van de monitoring:

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

Vervolgens, bij het starten van de service en het aanroepen van de methode voor het starten van de monitoring, zullen alle berichten uit de opgegeven wachtrij naar de bijbehorende processor gaan.

Voor het ontvangen van een enkel bericht in de interface-fabriek IMessagingComponentFactory is er een methode CreateMessageReceiver, die een ontvanger aanmaakt die wacht op berichten uit de opgegeven wachtrij:

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

Voor het versturen van een bericht moet gebruik worden gemaakt van dezelfde IMessagingComponentFactory en een zender van het bericht worden gemaakt:

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

Voor het serialiseren en deserialiseren van berichten zijn er drie standaardopties: eenvoudige tekst, XML en JSON, maar indien nodig kunnen we onze eigen implementaties van de interfaces maken. IMessageSerializer en IMessageDeserializer.

We hebben geprobeerd de unieke mogelijkheden van elke queue manager te behouden, bijvoorbeeld, ViennaNET.Messaging.MQSeriesQueue maakt het mogelijk om niet alleen tekstberichten, maar ook binaire berichten te verzenden, en ViennaNET.Messaging.RabbitMQQueue ondersteunt routing en het dynamisch creëren van queues. In onze adapterwrapper voor RabbitMQ is ook een soort RPC-implementatie gerealiseerd: we verzenden een bericht en wachten op een antwoord van een speciale tijdelijke queue die alleen voor één antwoordbericht wordt gemaakt.

Hier is voorbeeld van het gebruik van queues met de belangrijkste nuances van de verbinding.

ViennaNET.CallContext

We gebruiken queues niet alleen voor integratie tussen verschillende systemen, maar ook voor communicatie tussen microservices van één applicatie, bijvoorbeeld binnen het kader van een saga. Dit leidde tot de noodzaak om samen met het bericht aanvullende gegevens te versturen, zoals de gebruikersnaam, het verzoek-ID voor end-to-end logging, het IP-adres van de bron en de autorisatiegegevens. Voor het doorgeven van deze gegevens hebben we een bibliotheek ontwikkeld ViennaNET.CallContext, die het mogelijk maakt om gegevens uit het inkomende verzoek naar de service op te slaan. Hierbij maakt het niet uit op welke manier het verzoek is gedaan, via een queue of via Http. Vervolgens, voordat we het uitgangsverzoek of bericht verzenden, worden de gegevens uit de context gehaald en in de headers geplaatst. Op deze manier ontvangt de volgende service de aanvullende gegevens en kan er op dezelfde wijze mee worden omgegaan.

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

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster