بررسی عملی CQRS- بخش دوم: بررسی الگوی Mediator با استفاده از کتابخانه MediatR

در این بخش به یکی از ملزومات پیاده سازی CQRS یعنی الگوی Mediator و کتابخانه MediatR می پردازیم.



https://vrgl.ir/01pUA


در قسمت قبلی به طور اجمالی با CQRS آشنا شدیم و اشاره کردیم که الگوی Mediator به ارائه یک سولوشن تمیز در ارتباط با CQRS کمک شایانی میکند و باعث ایجاد Loose Coupling می شود. در این قسمت به بررسی کتابخانه معروف و محبوب MediatR از جیمی بوگارد می پردازیم.

در این مقاله از Gist استفاده شده است و لود شدن بخش مربوط به کد ها ممکن است کمی زمانبر باشد

الگوی Mediator

در الگوی Mediator دیگر کلاس ها و سرویس ها با یکدیگر مستقیم در ارتباط نیستند بلکه درخواست خود را از طریق یک واسط می فرستند و از طریق همان واسط، پاسخ مربوطه را دریافت میکنند. این کار باعث می شود که وابستگی شدید بین دو سرویس به حداقل برسد و Loose Coupling داشته باشیم که به توسعه و نگهداری یک پروژه کمک شایانی میکند.


کتابخانه MediatR

کتابخانه MediatR یک پیاده سازی از الگوی Mediator در دات نت می باشد که از Request و Command پشتیبانی میکند. در MediatR هر درخواست در یک صف در حافظه قرار میگیرد و سپس به Handler مربوط به آن درخواست وصل می شود. MediatR هیچ وابستگی به سایر کتابخانه ها ندارد و از Generic ها در سی شارپ برای ساخت Response و همچنین تعیین Handler ها استفاده میکند.

یکی دیگر از ویژگی های بسیار خوب MediatR، پشتیبانی از الگوی Pub/Sub می باشد که به وسیله آن میتوان یک notification ایجاد کرد و در جاهای مختلف پروژه این notification را صدا زد.


الگوی Pub/Sub با استفاده از Notification ها در MediatR
الگوی Pub/Sub با استفاده از Notification ها در MediatR


بررسی Request و Request Handler در MediatR

در MediatR برای رسیدگی به Request ها از Handler ها استفاده میکنیم. هر درخواست از اینترفیس IRequest (و یا مدل Generic آن ) ارث بری میکند. و Handler مربوط به آن از اینترفیس IRequestHandler<TRequest,TResponse ارث بری میکند. که TRequest همان درخواست می باشد که از IRequest ارث بری کرده است و به این ترتیب Request به Handler خود مپ میشود. در نمونه کد زیر یک Request و Request Handler مربوط به آن را میتوانید ببینید.

https://gist.github.com/babaktaremi/61bae27d61130fb1255fb7c17c2dd048
https://gist.github.com/babaktaremi/8cf76b9b457cae9f332517f4de1a525a

در مدل جنریک Handler، خروجی درخواست معلوم میشود که از جنس boolean می باشد. اینترفیس IRequestHandler یک متد به اسم Handle دارد که باید پیاده سازی شود.

سپس در کنترلر مربوطه، یک instance از IMediator را تزریق کرده و کلاس Request را به شکل زیر به آن پاس میدهیم.

https://gist.github.com/babaktaremi/23883059e79e13c8d14fef42f0c3620c



استفاده از MediatR در پروژه عملی


پس از ایجاد یک پروژه نمونه، پکیج مربوط به MediatR را از طریق Package Manager Console با دستور زیر نصب میکنیم:

Install-Package MediatR

و یا از طریق Manage Nugget Packages پکیج مربوط به MediatR را نصب میکنیم:


سپس نیاز است برای استفاده از MediatR در Dependency Injection پکیج MediatR.Extensions.Microsoft.DepenedencyInjection را نیز نصب کنیم. این پکیج اینترفیس Imediator و همچنین Pipeline ها و Handler و Request ها را به صورت Transient به سیستم تزریق میکند.

برای نصب میتوانیم از طریق Package Manager Console دستور زیر را اجرا نماییم:

Install-Package MediatR.Extensions.Microsoft.DependencyInjection 

و یا از طریق Manage Nugget Manager برای نصب اقدام نماییم

سپس در Startup.cs در قسمت Configure Services سرویس مربوط به MediatR را تزریق میکنیم.

https://gist.github.com/babaktaremi/d1bdee85964361c5a656ecee734280b1


به AddMediaR باید Assembly مربوط به قسمتی که Handler ها در آن قرار دارد را پاس بدهیم. در این پروژه چون Class Library جدا نداریم همان Assembly مربوط به Startup را پاس دادیم. تمامی تنظیمات مربوط به MediatR انجام شده است.


ایجاد اولین Request و Handler

برروی سولوشن راست کلیک کرده یک فولدر به نام Data ایجاد میکنیم. در این پوشه، پوشه دیگری به نام Model ایجاد میکنیم که در این پوشه قرار است مدل ها و انتیتی های پروژه قرار بگیرند.

در پوشه Model یک کلاس به نام Book ایجاد میکنیم

سپس در پوشه Model یک کلاس به نام Book ایجاد میکنیم که شامل Property های زیر می باشد.

https://gist.github.com/babaktaremi/0c2e6b9e6086392ee0edfb3d80d24fa0

این کلاس قرار است نقش Book Entity را بازی کند.

در پوشه Data، یک پوشه دیگر تحت عنوان Repositories می سازیم.سپس در پوشه Repository یک پوشه دیگر تحت عنوان BookRepository می سازیم که قرار است در این پوشه ریپوزیتوری مربوط به کلاس Book را بسازیم.

در این پروژه از In Memory Database استفاده میکنیم و لیستی از Book را درون حافظه ذخیره میکنیم و از آن نیز Book را باز خوانی میکنیم.

یک اینترفیس با نام IBookRepository می سازیم.

https://gist.github.com/babaktaremi/6aca444a32be029d3fbecfc1e63d27e1


سپس کلاسی با نام BookMockRepository می سازیم و این اینترفیس را در آن به شکل زیر پیاده سازی میکنیم.

https://gist.github.com/babaktaremi/3b7e83db74e035aca81f779d527ba332



سپس این اینترفیس را به صورت Scoped رجیستر میکنیم.

حال همه چیز برای ایجاد اولین Handler آماده است. در پروژه یک پوشه دیگر به نام Application ایجاد میکنیم. لایه Application وظیفه سازماندهی به ریکوئست ها را بر عهده دارد ( Request Orchestration). برای مطالعه بیشتر درباره لایه اپلیکیشن و همچنین لایه بندی پروژه میتوانید به این لینک مراجعه کنید.

در پوشه Application یک پوشه به نام BookApplication ایجاد میکنیم. در این پوشه یک پوشه دیگر به نام GetBookById ایجاد میکنیم.در این پوشه دو کلاس قرار میگیرد:

  • GetBookByIdRequest
  • GetBookByRequestHandler


کلاس GetBookByIdRequest از اینترفیس IRequest ارث بری میکند.این اینترفیس جنریک یک پارامتر ورودی دارد که نوع Response یک Request را مشخص میکند. که در اینجا Response درخواست GetBookByIdRequest یک Book می باشد.

https://gist.github.com/babaktaremi/2fecbc1dea3c97a07cd5b206973d6595

در قدم بعدی برای این Request یک Handler مینویسیم که وظیفه ایجاد Response برای GetBookByIdRequest را برعهده دارد. هر Handler از اینترفیس IRequestHandler ارث بری میکند. این اینترفیس دو پارامتر را به عنوان وروردی جنریک میگیرد، اولین پارامتر Request و دومین پارامتر Response مربوطه می باشد. این اینترفیس یک متد به نام Handle از جنس Task دارد که باید پیاده سازی شود.

در این Handler یک نمونه از IBookRepository را Inject میکنیم. در متد Handle با استفاده از BookRepository یک Instance از Book را به عنوان Response بر میگردانیم.

https://gist.github.com/babaktaremi/28f5cd4f1424097a0f0e908aa235dc15



سپس یک کنترلر برای استفاده از این Handler می سازیم و نام آن را BookController قرار میدهیم.

برای استفاده از Handler های MediatR ، اینترفیس IMeditor را در کنترلر تزریق میکنیم.برای دریافت Book از طریق Id، ریکوئست GetBookByIdRequest را به متد Send در اینترفیس IMediator پاس میدهیم.حال این متد یک Book را به ما بر میگرداند چرا که در تایپ IRequest یک Book را به عنوان Response تعریف کرده ایم.

https://gist.github.com/babaktaremi/41ab0e9c5f2ff30820d1033416f62eb6


به وسیله Swagger این API را چک میکنیم. و جواب یک Instance از Book می باشد.


آشنایی با Behavior های MediatR

با استفاده از Behavior ها در MediatR میتوان پایپ لاین های مخصوص ساخت که در حین، قبل و یا بعد اجرای یک Handler اجرا شوند.

میخواهیم با استفاده از Behavior ، درخواست هایی که به سمت هرکدام از Handler ها فرستاده میشوند را چک کنیم و در صورت وجود خطا،آن را لاگ کنیم.

در پوشه Application یک پوشه دیگر به نام Common می سازیم. در این پوشه یک کلاس به نا LoggingBehavior ایجاد میکنیم.



این کلاس را به صورت جنریک در می آوریم. برای ساخت پایپ لاین در MediatR باید کلاس، اینترفیس IPiplelineBehavior ارث بری کند. این اینترفیس یک متد به نام Handle و از جنس Task دارد که باید پیاده سازی شود.

https://gist.github.com/babaktaremi/7733bf688e8aadda7181064199b495a0

در قطعه کد بالا، یک اینستنس از ILogger را به پایپ لاین تزریق کردیم که در صورت وقوع خطا از آن برای لاگ کردن خطا استفاه میکنیم.

برای اینکه در ادامه Handler مربوطه صدا زده شود،نیاز است که next صدا زده شود. این عمل در اصل باعث تشکیل Pipleline میشود و باعث میشود که LoggingBehavior در مسیر یک درخواست برای رسیدن به یک Handler قرار بگیرد.

پس از ساخت Pipeline آن را در Startup رجیستر میکنیم.

https://gist.github.com/babaktaremi/774775084b7187adfaf9b8d6ef4de393

برای تست پایپ لاین، در GetBookByIdRequestHandler یک خطای تستی پرتاب میکنیم.

و مجدد Api ای که ساختیم را در Swagger صدا میزنیم. مشاهده میکنیم و که Pipeline ساخته شده حین درخواست، خطا را Catch و آن را لاگ کرده است و API در واقع خطا را دریافت نکرده است



در MediatR دو Behavior به صورت پیش فرض وجود دارد مه به صورت پیش فرض در MediatR رجیستر شدند و میتوان آنها را پیاده سازی و استفاده کرد.

  • IRequestPreProccessor

این پایپ لاین قبل از اجرای هر Handler اجرا میشود. این اینترفیس یک متد به نام Process دارد که نوع درخواست را در را میتوان به وسیله آن در اختیار داشت و با توجه به آن قبل از هر درخواست یک سری عملیات را انجام داد.

https://gist.github.com/babaktaremi/a8d52d5e8f0ed8f713bc4caaca37ad13


  • IRequestPostProcessor

این پایپ لاین بعد از اجرای هر Handler اجرا میشود. این اینترفیس یک متد به نام Process دارد که به وسیله آن میتوان نوع درخواست و همچنین نوع Response را در اختیار داشت و با توجه به آنها یک سری عملیات انجام داد.

https://gist.github.com/babaktaremi/13e98c01eb53b578b0a7a99989327879


پیاده سازی الگوی Unit of Work توسط Post Processor در MediatR

همانطور که میدانید، Post Processor ها بعد از اجرای هر Handler اجرا میشوند و توسط آنها میتوان به نوع درخواست و همچنین نوع Response دسترسی داشت. در این قسمت توسط یک Post Processor میخواهیم الگوی Unit of Work را پیاده سازی کنیم. برای مطالعه درباره Unit of Work میتوانید به این لینک مراجعه کنید

در پوشه Common یک اینترفیس خالی با نام ICommitable ایجاد میکنیم. بوسیله این اینترفیس میخواهیم تمامی درخواست هایی که قرار است در دیتابیس تغییرات داشته باشند را علامتگذاری کنیم.

https://gist.github.com/babaktaremi/fc3c09494801509af5834846ac2489b9

در پوشه BookApplication، یک پوشه جدید به نام AddBook ایجاد میکنیم. در این پوشه یک کلاس به نام AddBookRequest به شکل زیر ایجاد میکنیم. این کلاس از اینترفیس ICommitable ارث بری میکند و به نوعی آن را با اینترفیس ICommitable علامت گذاری میکنیم.

https://gist.github.com/babaktaremi/c084475ce6485efa18738d015f98ffac


هندلر مربوط به این Request را به شکل زیر میسازیم.دقت کنید که برای اضافه کردن مدل جدید، از Mock Repository که قبلتر ساختیم استفاده میکنیم.

https://gist.github.com/babaktaremi/396b01cb184703d7f55121720977bf4b

برای پیاده سازی الگوی Unit of Work به سراغ پیاده سازی Post Processor آن میرویم. در پوشه Common یک کلاس به نام CommitPostProcessor می سازیم. در اینجا برای نمونه در کنسول لاگ ثبت میکنیم و به نوعی ثبت داده در دیتابیس را شبیه سازی میکنیم.

https://gist.github.com/babaktaremi/f561528ebc17253afc303b85c9816332

همانطور که میدانید Post Processor ها در انتهای هر Request به MediatR اجرا میشوند، پس در اینجا در انتهای هر Request چک میکنیم که آیا Request با ICommitable علامت گذاری شده است یا خیر و اگر علامت گذاری شده بود تغییرات را در دیتابیس اعمال میکنیم. به طور مثال اگر از EF Core استفاده میکنید میتوانید در اینجا متد Save Changes روی DbContext را صدا بزنید.(برای مشاهده منبع اصلی این قسمت میتوانید به این لینک در گیت هاب مراجعه کنید.)

در نهایت API Action مربوطه را ساخته و با استفاده از اینترفیس IMediator این درخواست را صدا میزنیم.

https://gist.github.com/babaktaremi/b73f23b195c1b1e68b7fbff3936ebf77


بررسی الگوی Pub/Sub

در الگوی Pub/Sub دو موجودیت داریم. Publisher و Subscriber که در آن Publisher یک Event را انتشار میدهد و Subscriber های آن Event بسته به شرایط یک سری عملیات خاص را مدیریت و اجرا میکنند. برای اینکه یک Subscriber از Event با خبر باشد، حتما باید عضوی از آن Event باشد که Publisher آن را Invoke میکند

این الگو برای ایجاد Loose Coupling و همچنین در Distributed System ها بسیار کاربرد دارد.

برای مطالعه بیشتر در مورد پیاده سازی الگوی Pub/Sub در سی شارپ با استفاده از Event Handler ها میتوانید به این لینک مراجعه کنید.


پیاده سازی الگوی Pub/Sub در MediatR

پیاده سازی الگوی Pub/Sub در MediatR بسیار ساده است. در MediatR مفهومی به اسم Notification وجود دارد. یک کلاس از اینترفیس INotification ارث بری میکند. این کلاس نمایانگر یک Event در MediatR میباشد که بقیه Handler ها و یا کلاس ها میتوانند این کلاس را Publish کنند. سپس Handler مربوط به این Event ( که از INotificationHandler ارث بری کرده است) اقدامات مربوط به این Event را انجام میدهد.

از Notification ها میتوان برای انجام کار هایی که بین چند Handler مشترک هستند استفاده کرد.

ایجاد یک Notification

فرض کنید میخواهیم به ازای ایجاد هر Book یک Email به ادمین وب سایت ارسال کنیم. ابتدا در پوشه Common یک پوشه دیگر به نام Notifications ایجاد میکنیم.

سپس در این پوشه یک پوشه دیگر به نام EmailNotification ایجاد میکنیم.در این پوشه دو کلاس به نام های EmailNotification و EmailNotificationHandler ایجاد میکنیم.

کلاس EmailNotification را به شکل زیر ایجاد میکنیم.

https://gist.github.com/babaktaremi/2241362c1c4c101473ad7f99ac648990

دقت کنید که این کلاس از INotification ارث بری میکند و شامل دو پراپرتی Email Address و Email Content می باشد که باید مقدار دهی شوند.

سپس کلاس EmailNotificationHandler را به شکل زیر پیاده سازی میکنیم. برای شبیه سازی، به جای ارسال ایمیل، یک لاگ در کنسول ثبت میکنیم.

https://gist.github.com/babaktaremi/a98a84a029602da376117fd064fa2bc0

به BookController بر میگردیم. در اکشن CreateBook برای استفاده از این Notification از متد Publish اینترفیس IMediator استفاده میکنیم.


https://gist.github.com/babaktaremi/dd1d39c1d4232f71fb158155b53262a7

در خط 8 کد بالا، Notification مربوط به ارسال Email را پابلیش کرده ایم. در کنسول خواهیم داشت:

مشاهده میشود که Event مربوطه به ارسال Notification توسط EmailNotifcation Handler اجرا شده است.

نتیجه گیری

در این قسمت به بررسی MediatR پرداختیم و با ویژگی های آن آشنا شدیم. پیاده سازی الگوی Mediator با استفاده از کتابخانه MeidatR کاری بسیار راحت و بی دردسر است که به ارائه سولوشن تمیز و ایجاد Loose Coupling (که از ملزومات پیاده سازی CQRS می باشد) کمک بسیازی میکند. همچنین در MediatR میتوان Pipeline های اختصاصی ایجاد کرد و با استفاده از Post Processor ها الگوی Unit of Work را پیاده سازی کرد. در قسمت بعد به پیاده سازی پروژه به روش CQRS با استفاده از MediatR می پردازیم.

اگر به سورس کد این قسمت نیاز داشتید میتوانید آن را لینک زیر در گیت هاب دریافت نمایید. و همچنین اگر سوال یا مطلبی بود، خوشحال میشوم که آن را در بخش نظرات مطرح فرمایید.

https://github.com/babaktaremi/MediatRExplore

مقالات بیشتر در دات نت زوم

https://t.me/DotNetZoom