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

Дадаць каментар