Спільнота .NET-розробників Райффайзенбанку продовжує короткий аналіз вмісту ViennaNET. Про те, як і навіщо ми до цього дійшли,
У цій статті пройдемося ще не розглянутими бібліотеками для роботи з розподіленими транзакціями, чергами та БД, які можна знайти в нашому репозиторії на GitHub (
ViennaNET.Sagas
Коли в проекті відбувається перехід на DDD та мікросервісну архітектуру, то при рознесенні бізнес-логіки з різних сервісів виникає проблема, пов'язана з необхідністю реалізації механізму розподілених транзакцій, адже багато сценаріїв часто зачіпають відразу кілька доменів. З такими механізмами докладніше можна познайомитись, наприклад,
У наших проектах ми реалізували простий, але корисний механізм: сага, а точніше, сага на основі оркестрації. Суть її в наступному: є бізнес-сценарій, в якому необхідно послідовно здійснити операції в різних сервісах, при цьому, у разі виникнення будь-яких проблем на будь-якому кроці, необхідно викликати процедуру відкату всіх попередніх кроків, де вона передбачена. Таким чином, наприкінці виконання саги, незалежно від успішності, ми отримуємо консистентні дані у всіх доменах.
Наша реалізація поки що зроблена в базовому вигляді і не зав'язана на використанні будь-яких способів взаємодії з іншими сервісами. Застосовувати її нескладно: достатньо зробити спадкоємця від базового абстрактного класу SagaBase, де T – це ваш клас контексту, в якому можна зберігати вихідні дані, необхідні для роботи саги, а також деякі проміжні результати. Екземпляр контексту буде прокидатися на всі кроки під час виконання. Сама сага є stateless класом, тому екземпляр може бути поміщений в DI як Singleton, щоб отримати необхідні залежності.
Приклад оголошення:
public class ExampleSaga : SagaBase<ExampleContext>
{
public ExampleSaga()
{
Step("Step 1")
.WithAction(c => ...)
.WithCompensation(c => ...);
AsyncStep("Step 2")
.WithAction(async c => ...);
}
}
Приклад виклику:
var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);
Повноцінні приклади різних реалізацій можна переглянути
ViennaNET.Orm.*
Набір бібліотек для роботи з різними базами даних через Nhibernate. У нас використовується підхід DB-First із застосуванням Liquibase, тому тут присутній тільки функціонал роботи з даними в готовій БД.
ViennaNET.Orm.Seedwork и ViennaNET.Orm
- Основні складання, що містять базові інтерфейси та їх реалізації відповідно. Зупинимося на їхньому вмісті докладніше.
Інтерфейс IEntityFactoryService
та його реалізація EntityFactoryService
є головною відправною точкою до роботи з БД, оскільки тут створюється Unit of Work, репозиторії до роботи з конкретними сутностями, і навіть виконавці команд і прямих SQL-запросов. Іноді зручно обмежити можливості класу роботи з БД, наприклад, дати можливість лише читання даних. Для таких випадків у IEntityFactoryService
є предок – інтерфейс IEntityRepositoryFactory
, в якому оголошено лише метод для створення репозиторіїв.
Для безпосереднього звернення до бази даних використовується механізм провайдерів. Для кожної СУБД, яка використовується у нас в командах, є своя реалізація: ViennaNET.Orm.MSSQL, ViennaNET.Orm.Oracle, ViennaNET.Orm.SQLite, ViennaNET.Orm.PostgreSql
.
При цьому в одному додатку може бути зареєстровано кілька провайдерів одночасно, що дозволяє, наприклад, у рамках одного сервісу без будь-яких витрат на доопрацювання інфраструктури провести покрокову міграцію з однієї СУБД на іншу. Механізм вибору необхідного підключення і, отже, провайдера для конкретного класу-сутності (для якого і пишеться мапінг на таблиці БД) реалізований через реєстрацію сутності в класі BoundedContext (містить метод для реєстрації доменних сутностей) або його спадкоємця ApplicationContext (містить методи для реєстрації аплікаційних сутностей) , прямих запитів та команд), де в якості аргументу приймається ідентифікатор підключення з конфігурації:
"db": [
{
"nick": "mssql_connection",
"dbServerType": "MSSQL",
"ConnectionString": "...",
"useCallContext": true
},
{
"nick": "oracle_connection",
"dbServerType": "Oracle",
"ConnectionString": "..."
}
],
Приклад ApplicationContext:
internal sealed class DbContext : ApplicationContext
{
public DbContext()
{
AddEntity<SomeEntity>("mssql_connection");
AddEntity<MigratedSomeEntity>("oracle_connection");
AddEntity<AnotherEntity>("oracle_connection");
}
}
Якщо ідентифікатор підключення не вказано, використовуватиметься підключення з ім'ям «default».
Безпосередньо мапінг сутностей таблиці БД реалізується стандартними засобами NHibernate. Можна використовувати опис як через XML-файли, так і через класи. Для зручного написання репозиторіїв-заглушок у Unit-тестах є бібліотека ViennaNET.TestUtils.Orm
.
Повноцінні приклади використання ViennaNET.Orm.* можна знайти
ViennaNET.Messaging.*
Набір бібліотек для роботи з чергами.
Для роботи з чергами було обрано такий самий підхід, що і з різними СУБД, а саме – максимально можливий уніфікований підхід з погляду роботи з бібліотекою, незалежно від менеджера черг, що використовується. Бібліотека ViennaNET.Messaging
якраз відповідає за цю уніфікацію, а ViennaNET.Messaging.MQSeriesQueue, ViennaNET.Messaging.RabbitMQQueue и ViennaNET.Messaging.KafkaQueue
містять реалізації адаптерів для IBM MQ, RabbitMQ та Kafka відповідно.
У роботі з чергами є два процеси: отримання повідомлення та відправлення.
Розглянемо одержання. Тут є 2 варіанти: для постійного прослуховування та отримання одиничного повідомлення. Для постійного прослуховування черги необхідно спочатку описати клас процесора, успадкований від IMessageProcessor
, який відповідатиме за обробку вхідного повідомлення. Далі його необхідно «прив'язати» до певної черги, робиться це через реєстрацію в IQueueReactorFactory
із зазначенням ідентифікатора черги із конфігурації:
"messaging": {
"ApplicationName": "MyApplication"
},
"rabbitmq": {
"queues": [
{
"id": "myQueue",
"queuename": "lalala",
...
}
]
},
Приклад запуску прослуховування:
_queueReactorFactory.Register<MyMessageProcessor>("myQueue");
var queueReactor = queueReactorFactory.CreateQueueReactor("myQueue");
queueReactor.StartProcessing();
Потім, при старті сервісу та виклику методу для початку прослуховування, всі повідомлення із зазначеної черги потраплятимуть у відповідний процесор.
Для отримання одиничного повідомлення в інтерфейсі-фабриці IMessagingComponentFactory
є метод CreateMessageReceiver
, який створить одержувача, який очікує повідомлення із зазначеної йому черги:
using (var receiver = _messagingComponentFactory.CreateMessageReceiver<TestMessage>("myQueue"))
{
var message = receiver.Receive();
}
Для надсилання повідомлення необхідно скористатися все тією ж IMessagingComponentFactory
та створити відправника повідомлення:
using (var sender = _messagingComponentFactory.CreateMessageSender<MyMessage>("myQueue"))
{
sender.SendMessage(new MyMessage { Value = ...});
}
Для серіалізації та десереалізації повідомлення є три готові варіанти: просто текст, XML і JSON, але при необхідності спокійно можна зробити свої реалізації інтерфейсів IMessageSerializer и IMessageDeserializer
.
Ми постаралися зберегти унікальні можливості кожного менеджера черг, наприклад, ViennaNET.Messaging.MQSeriesQueue
дозволяє відправляти не лише текстові, а й байтові повідомлення, а ViennaNET.Messaging.RabbitMQQueue
підтримує роутинг та створення черг "на льоту". У нашій обгортці адаптера для RabbitMQ також реалізована деяка подоба RPC: відправляємо повідомлення і очікуємо відповіді зі спеціальної тимчасової черги, яка створюється тільки для одного повідомлення у відповідь.
Ось
ViennaNET.CallContext
Ми використовуємо черги не тільки для інтеграції між різними системами, але й для спілкування між мікросервісами однієї програми, наприклад, у рамках саги. Це призвело до необхідності передачі разом з повідомленням таких допоміжних даних, як логін користувача, ідентифікатор запиту наскрізного логування, ip-адреса джерела та авторизаційні дані. Для реалізації прокидання цих даних розробили бібліотеку ViennaNET.CallContext
, яка дозволяє зберігати дані з запиту, що входить в сервіс. При цьому те, як було зроблено запит, через чергу або через Http, не відіграє ролі. Потім перед надсиланням вихідного запиту або повідомлення дістаються дані з контексту і поміщаються в заголовки. Таким чином, наступний сервіс отримує допоміжні дані та аналогічно ними розпоряджається.
Дякуємо за увагу, чекаємо на ваші коментарі та pull request-ів!
Джерело: habr.com