ViennaNET: zestaw bibliotek dla backendu. Część 2

Społeczność programistów Raiffeisenbank .NET w dalszym ciągu krótko przegląda zawartość ViennaNET. O tym jak i dlaczego do tego doszliśmy, możesz przeczytać pierwszą część.

W tym artykule omówimy jeszcze nierozważone biblioteki do pracy z rozproszonymi transakcjami, kolejkami i bazami danych, które można znaleźć w naszym repozytorium GitHub (źródła są tutaj) i Pakiety Nuget tutaj.

ViennaNET: zestaw bibliotek dla backendu. Część 2

ViennaNET.Sagas

Kiedy projekt przechodzi na architekturę DDD i mikroserwisową, wówczas gdy logika biznesowa jest rozproszona pomiędzy różne usługi, pojawia się problem związany z koniecznością wdrożenia mechanizmu transakcji rozproszonych, ponieważ wiele scenariuszy często wpływa na kilka domen jednocześnie. Możesz zapoznać się z takimi mechanizmami bardziej szczegółowo, na przykład w książce „Wzorce mikrousług” Chrisa Richardsona.

W naszych projektach zaimplementowaliśmy prosty, ale przydatny mechanizm: sagę, a raczej sagę opartą na orkiestracji. Jego istota jest następująca: istnieje pewien scenariusz biznesowy, w którym konieczne jest sekwencyjne wykonywanie operacji w różnych usługach, a jeśli na którymkolwiek etapie pojawią się jakieś problemy, konieczne jest wywołanie procedury wycofania wszystkich poprzednich kroków, gdzie pod warunkiem, że. Tym samym na koniec sagi, niezależnie od powodzenia, otrzymujemy spójne dane we wszystkich domenach.

Nasze wdrożenie nadal odbywa się w podstawowej formie i nie jest związane z wykorzystaniem jakichkolwiek metod interakcji z innymi usługami. Nie jest to trudne w użyciu: wystarczy utworzyć potomka podstawowej klasy abstrakcyjnej SagaBase<T>, gdzie T jest klasą kontekstową, w której możesz przechowywać początkowe dane niezbędne do działania sagi, a także niektóre wyniki pośrednie. Instancja kontekstu zostanie przekazana do wszystkich kroków podczas wykonywania. Sama Saga jest klasą bezstanową, więc instancję można umieścić w DI jako Singleton, aby uzyskać niezbędne zależności.

Przykładowa reklama:

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

Przykładowe wywołanie:

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

Można wyświetlić pełne przykłady różnych wdrożeń tutaj i w montażu z testy.

ViennaNET.Orm.*

Zestaw bibliotek do pracy z różnymi bazami danych za pośrednictwem Nhibernate. Stosujemy podejście DB-First wykorzystując Liquibase, więc istnieje tylko funkcjonalność do pracy z danymi w gotowej bazie danych.

ViennaNET.Orm.Seedwork и ViennaNET.Orm – główne zespoły zawierające odpowiednio podstawowe interfejsy i ich implementacje. Przyjrzyjmy się ich zawartości bardziej szczegółowo.

Interfejs IEntityFactoryService i jego wdrożenie EntityFactoryService są głównym punktem wyjścia do pracy z bazą danych, gdyż tutaj tworzone są Jednostka Pracy, repozytoria do pracy z konkretnymi podmiotami, a także wykonawcy poleceń i bezpośrednich zapytań SQL. Czasami wygodnie jest ograniczyć możliwości klasy do pracy z bazą danych, na przykład aby zapewnić możliwość tylko odczytu danych. Dla takich przypadków IEntityFactoryService istnieje przodek - interfejs IEntityRepositoryFactory, który deklaruje jedynie metodę tworzenia repozytoriów.

Aby uzyskać bezpośredni dostęp do bazy danych, wykorzystywany jest mechanizm dostawcy. Każdy DBMS, którego używamy w naszych zespołach, ma swoją własną implementację: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.

Jednocześnie w jednej aplikacji można zarejestrować kilku dostawców jednocześnie, co pozwala np. w ramach jednej usługi, bez ponoszenia kosztów modyfikacji infrastruktury, przeprowadzić krok po kroku migrację z jednego DBMS do drugiego. Mechanizm wyboru wymaganego połączenia, a co za tym idzie dostawcy dla konkretnej klasy encji (dla której pisane jest mapowanie na tabele bazy danych) realizowany jest poprzez rejestrację encji w klasie BoundedContext (zawiera metodę rejestracji encji domenowych) lub jej następcy ApplicationContext (zawiera metody rejestracji encji aplikacji, bezpośrednich żądań i poleceń), gdzie jako argument akceptowany jest identyfikator połączenia z konfiguracji:

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

Przykładowy kontekst aplikacji:

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

Jeśli identyfikator połączenia nie zostanie określony, użyte zostanie połączenie o nazwie „domyślne”.

Bezpośrednie mapowanie encji do tabel bazy danych realizowane jest przy użyciu standardowych narzędzi NHibernate. Możesz użyć opisu zarówno poprzez pliki XML, jak i poprzez klasy. Do wygodnego pisania repozytoriów pośredniczących w testach jednostkowych dostępna jest biblioteka ViennaNET.TestUtils.Orm.

Pełne przykłady wykorzystania ViennaNET.Orm.* można znaleźć tutaj.

ViennaNET.Messaging.*

Zestaw bibliotek do pracy z kolejkami.

Do pracy z kolejkami wybrano to samo podejście, co w przypadku różnych systemów DBMS, a mianowicie maksymalnie możliwe ujednolicone podejście do pracy z biblioteką, niezależnie od używanego menedżera kolejek. Biblioteka ViennaNET.Messaging jest właśnie odpowiedzialny za to zjednoczenie, i ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue и ViennaNET.Messaging.KafkaQueue zawierają implementacje adapterów odpowiednio dla IBM MQ, RabbitMQ i Kafka.

Podczas pracy z kolejkami istnieją dwa procesy: odebranie wiadomości i wysłanie jej.

Rozważ otrzymanie. Są tu 2 możliwości: ciągłego słuchania i odbierania pojedynczej wiadomości. Aby stale nasłuchiwać kolejki, należy najpierw opisać klasę procesora, z której jest dziedziczony IMessageProcessor, który będzie odpowiedzialny za przetwarzanie przychodzącej wiadomości. Następnie należy ją „połączyć” z konkretną kolejką, odbywa się to poprzez rejestrację IQueueReactorFactory wskazanie identyfikatora kolejki z konfiguracji:

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

Przykład rozpoczęcia słuchania:

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

Następnie, gdy usługa zostanie uruchomiona i zostanie wywołana metoda rozpoczęcia nasłuchiwania, wszystkie komunikaty z określonej kolejki trafią do odpowiedniego procesora.

Aby otrzymać pojedynczą wiadomość w interfejsie fabrycznym IMessagingComponentFactory istnieje metoda CreateMessageReceiverco utworzy odbiorcę oczekującego na wiadomość z określonej dla niego kolejki:

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

Aby wysłać wiadomość musisz użyć tego samego IMessagingComponentFactory i utwórz nadawcę wiadomości:

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

Istnieją trzy gotowe opcje serializacji i deserializacji wiadomości: sam tekst, XML i JSON, ale w razie potrzeby możesz łatwo wykonać własne implementacje interfejsu IMessageSerializer и IMessageDeserializer.

Staraliśmy się zachować unikalne możliwości każdego menedżera kolejek, m.in. ViennaNET.Messaging.MQSeriesQueue umożliwia wysyłanie nie tylko wiadomości tekstowych, ale także wiadomości bajtowych i ViennaNET.Messaging.RabbitMQQueue obsługuje routing i kolejkowanie w locie. Nasze opakowanie adaptera dla RabbitMQ również implementuje coś na kształt RPC: wysyłamy wiadomość i czekamy na odpowiedź ze specjalnej tymczasowej kolejki, która jest tworzona tylko dla jednej wiadomości z odpowiedzią.

tutaj jest przykład wykorzystania kolejek z podstawowymi niuansami połączenia.

ViennaNET.CallContext

Kolejek używamy nie tylko do integracji pomiędzy różnymi systemami, ale także do komunikacji pomiędzy mikroserwisami tej samej aplikacji, np. w ramach sagi. Spowodowało to konieczność przesyłania wraz z komunikatem takich danych pomocniczych jak login użytkownika, identyfikator żądania kompleksowego logowania, źródłowy adres IP oraz dane autoryzacyjne. Aby zaimplementować przekazywanie tych danych, opracowaliśmy bibliotekę ViennaNET.CallContext, co pozwala na przechowywanie danych z żądania wchodzącego do usługi. W tym przypadku nie ma znaczenia, w jaki sposób żądanie zostało złożone – poprzez kolejkę czy przez HTTP. Następnie przed wysłaniem wychodzącego żądania lub wiadomości pobierane są dane z kontekstu i umieszczane w nagłówkach. Tym samym kolejny serwis otrzymuje dane pomocnicze i zarządza nimi w ten sam sposób.

Dziękujemy za uwagę, czekamy na Twoje komentarze i prośby o ściągnięcie!

Źródło: www.habr.com

Dodaj komentarz