ViennaNET: набір бібліотек для backend'а. Частина 2

Спільнота .NET-розробників Райффайзенбанку продовжує короткий аналіз вмісту ViennaNET. Про те, як і навіщо ми до цього дійшли, можна почитати у першій частині.

У цій статті пройдемося ще не розглянутими бібліотеками для роботи з розподіленими транзакціями, чергами та БД, які можна знайти в нашому репозиторії на GitHub (вихідники лежать тут), А Nuget-пакети тут.

ViennaNET: набір бібліотек для backend'а. Частина 2

ViennaNET.Sagas

Коли в проекті відбувається перехід на DDD та мікросервісну архітектуру, то при рознесенні бізнес-логіки з різних сервісів виникає проблема, пов'язана з необхідністю реалізації механізму розподілених транзакцій, адже багато сценаріїв часто зачіпають відразу кілька доменів. З такими механізмами докладніше можна познайомитись, наприклад, у книзі "Microservices Patterns", Chris Richardson.

У наших проектах ми реалізували простий, але корисний механізм: сага, а точніше, сага на основі оркестрації. Суть її в наступному: є бізнес-сценарій, в якому необхідно послідовно здійснити операції в різних сервісах, при цьому, у разі виникнення будь-яких проблем на будь-якому кроці, необхідно викликати процедуру відкату всіх попередніх кроків, де вона передбачена. Таким чином, наприкінці виконання саги, незалежно від успішності, ми отримуємо консистентні дані у всіх доменах.

Наша реалізація поки що зроблена в базовому вигляді і не зав'язана на використанні будь-яких способів взаємодії з іншими сервісами. Застосовувати її нескладно: достатньо зробити спадкоємця від базового абстрактного класу 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

Додати коментар або відгук