Społeczność programistów .NET w Raiffeisenbanku kontynuuje krótkie omówienie zawartości ViennaNET. O tym, jak i dlaczego do tego doszliśmy, .
W tym artykule omówimy jeszcze nieprzeanalizowane biblioteki do pracy z rozproszonymi transakcjami, kolejkami i bazami danych, które można znaleźć w naszym repozytorium na GitHubie (), a .
ViennaNET.Sagas
Gdy w projekcie następuje przejście na DDD i architekturę mikroserwisów, podczas rozdzielania logiki biznesowej na różne usługi pojawia się problem związany z koniecznością zaimplementowania mechanizmu rozproszonych transakcji, ponieważ wiele scenariuszy często obejmuje kilka domen. Z takimi mechanizmami można zapoznać się bardziej szczegółowo, na przykład, .
W naszych projektach wdrożyliśmy prosty, ale przydatny mechanizm: sagę, a dokładniej sagę opartą na orkiestracji. Jej istota jest następująca: istnieje pewien scenariusz biznesowy, w którym konieczne jest kolejno dokonanie operacji w różnych usługach, przy czym w przypadku wystąpienia jakichkolwiek problemów na którymkolwiek etapie, należy wywołać procedurę wycofania wszystkich wcześniejszych kroków, gdzie jest to przewidziane. W ten sposób na koniec wykonania sagi, niezależnie od sukcesu, otrzymujemy spójne dane we wszystkich domenach.
Nasza implementacja na razie jest w podstawowej formie i nie jest związana z używaniem jakichkolwiek metod interakcji z innymi usługami. Jej zastosowanie jest proste: wystarczy utworzyć dziedziczenie od podstawowej klasy abstrakcyjnej SagaBase, gdzie T to twój klasa kontekstu, w której można przechowywać dane źródłowe niezbędne do działania sagi oraz niektóre wyniki pośrednie. Instancja kontekstu będzie przekazywana przez wszystkie etapy podczas wykonywania. Sama saga jest klasą stateless, więc instancja może być przechowywana w DI jako Singleton, aby uzyskać niezbędne zależności.
Przykład deklaracji:
public class ExampleSaga : SagaBase<ExampleContext>
{
public ExampleSaga()
{
Step("Krok 1")
.WithAction(c => ...)
.WithCompensation(c => ...);
AsyncStep("Krok 2")
.WithAction(async c => ...);
}
}
Przykład wywołania:
var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);
Pełne przykłady różnych realizacji można zobaczyć i w zbiorze z .
ViennaNET.Orm.*
Zestaw bibliotek do pracy z różnymi bazami danych za pomocą Nhibernate. Używamy podejścia DB-First z zastosowaniem Liquibase, dlatego tutaj znajduje się tylko funkcjonalność do pracy z danymi w gotowej bazie danych.
ViennaNET.Orm.Seedwork oraz ViennaNET.Orm to główne zbiory, które zawierają podstawowe interfejsy oraz ich implementacje. Zatrzymajmy się na ich zawartości nieco dokładniej.
Interfejs IEntityFactoryService i jego implementacja EntityFactoryService są głównym punktem wyjścia do pracy z bazą danych, ponieważ tutaj tworzy się jednostkę pracy, repozytoria do pracy z konkretnymi encjami, 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, umożliwiając tylko odczyt danych. W takich przypadkach ma IEntityFactoryService przodek – interfejs IEntityRepositoryFactory, w którym zadeklarowano tylko metodę do tworzenia repozytoriów.
Do bezpośredniej interakcji z bazą danych stosowany jest mechanizm dostawców. Dla każdej używanej w naszych zespołach bazy danych istnieje jej implementacja: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql.
Przy tym w jednej aplikacji może być zarejestrowanych wiele dostawców jednocześnie, co pozwala na przykład w ramach jednego serwisu na przeprowadzenie stopniowej migracji z jednej bazy danych na inną bez jakichkolwiek kosztów związanych z poprawą infrastruktury. Mechanizm wyboru odpowiedniego połączenia, a co za tym idzie, dostawcy dla konkretnej klasy-encji (dla której pisany jest mapping na tabele bazy danych) zrealizowany jest przez rejestrację encji w klasie BoundedContext (zawierającej metodę rejestracji encji domenowych) lub jej dziedzica ApplicationContext (zawierającej metody rejestracji encji aplikacyjnych, bezpośrednich zapytań i poleceń), gdzie jako argument przyjmowany jest identyfikator połączenia z konfiguracji:
"db": [
{
"nick": "mssql_connection",
"dbServerType": "MSSQL",
"ConnectionString": "...",
"useCallContext": true
},
{
"nick": "oracle_connection",
"dbServerType": "Oracle",
"ConnectionString": "..."
}
],
Przykład ApplicationContext:
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 podany, używane będzie połączenie o nazwie „default.”
Mapowanie bytów na tabele bazy danych jest realizowane standardowymi środkami NHibernate. Można używać opisów zarówno w formacie xml, jak i przez klasy. Aby ułatwić pisanie zastępczych repozytoriów w testach jednostkowych, dostępna jest biblioteka ViennaNET.TestUtils.Orm.
Pełne przykłady użycia ViennaNET.Orm.* można znaleźć .
ViennaNET.Messaging.*
Zestaw bibliotek do pracy z kolejkami.
Do pracy z kolejkami zastosowano podobne podejście, jak w przypadku różnych systemów zarządzania bazami danych, a mianowicie – maksymalnie ujednolicone podejście z punktu widzenia pracy z biblioteką, niezależnie od używanego menedżera kolejek. Biblioteka ViennaNET.Messaging odpowiada za tę ujednolicenie, a ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue i ViennaNET.Messaging.KafkaQueue zawierają implementacje adapterów dla IBM MQ, RabbitMQ i Kafka odpowiednio.
W pracy z kolejkami są dwa procesy: odbieranie wiadomości i wysyłanie.
Rozważmy odbieranie. Są dwa warianty: dla stałego nasłuchiwania i dla odbierania pojedynczej wiadomości. Aby stale nasłuchiwać kolejkę, należy najpierw opisać klasę procesora, dziedziczącą po IMessageProcessor, która będzie odpowiedzialna za przetwarzanie przychodzącej wiadomości. Następnie należy 'przypiąć' ją do określonej kolejki, co osiąga się poprzez rejestrację w IQueueReactorFactory z podaniem identyfikatora kolejki z konfiguracji:
"messaging": {
"ApplicationName": "MyApplication"
},
"rabbitmq": {
"queues": [
{
"id": "myQueue",
"queuename": "lalala",
...
}
]
},
Przykład uruchomienia nasłuchiwania:
_queueReactorFactory.Register<MyMessageProcessor>("myQueue");
var queueReactor = queueReactorFactory.CreateQueueReactor("myQueue");
queueReactor.StartProcessing();
Następnie, przy uruchamianiu usługi i wywołaniu metody rozpoczynającej nasłuchiwanie, wszystkie wiadomości z określonej kolejki będą trafiały do odpowiedniego procesora.
Aby odebrać pojedynczą wiadomość w interfejsie fabrycznym IMessagingComponentFactory jest metoda CreateMessageReceiver, która utworzy odbiornik, oczekujący na wiadomości z określonej mu kolejki:
using (var receiver = _messagingComponentFactory.CreateMessageReceiver<TestMessage>("myQueue"))
{
var message = receiver.Receive();
}
Aby wysłać wiadomość należy skorzystać z tej samej IMessagingComponentFactory i utworzyć nadawcę wiadomości:
using (var sender = _messagingComponentFactory.CreateMessageSender<MyMessage>("myQueue"))
{
sender.SendMessage(new MyMessage { Value = ...});
}
Do serializacji i deserializacji wiadomości są trzy gotowe opcje: zwykły tekst, XML i JSON, ale w razie potrzeby można spokojnie stworzyć własne implementacje interfejsów. IMessageSerializer i IMessageDeserializer.
Staraliśmy się zachować unikalne możliwości każdego menedżera kolejek, na przykład: ViennaNET.Messaging.MQSeriesQueue umożliwia wysyłanie nie tylko wiadomości tekstowych, ale także bajtowych, a ViennaNET.Messaging.RabbitMQQueue obsługuje routingi i tworzenie kolejek „na żywo”. W naszym adapterze dla RabbitMQ również zaimplementowano coś w rodzaju RPC: wysyłamy wiadomość i czekamy na odpowiedź z specjalnej tymczasowej kolejki, która jest tworzona tylko dla jednej wiadomości zwrotnej.
Oto .
ViennaNET.CallContext
Używamy kolejek nie tylko do integracji między różnymi systemami, ale także do komunikacji między mikroserwisami jednego aplikacji, na przykład w ramach sagi. Doprowadziło to do konieczności przesyłania razem z wiadomością takich danych pomocniczych jak login użytkownika, identyfikator zapytania dla pełnego logowania, adres IP źródła oraz dane uwierzytelniające. W celu realizacji przekazywania tych danych opracowano bibliotekę ViennaNET.CallContext, która pozwala na przechowywanie danych ze wchodzącego żądania do usługi. Przy tym sposób, w jaki złożono zapytanie, przez kolejkę czy przez Http, nie ma znaczenia. Następnie, przed wysłaniem wychodzącego żądania lub wiadomości, pobierane są dane z kontekstu i umieszczane w nagłówkach. W ten sposób następny serwis otrzymuje dane pomocnicze i podobnie nimi zarządza.
Dziękujemy za uwagę, czekamy na wasze komentarze i pull requesty!
Źródło: habr.com
