Супольнасць. 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