C# enthusiast. NET foundation member
بررسی عملی CQRS- بخش دوم: بررسی الگوی Mediator با استفاده از کتابخانه MediatR
در این بخش به یکی از ملزومات پیاده سازی CQRS یعنی الگوی Mediator و کتابخانه MediatR می پردازیم.
در قسمت قبلی به طور اجمالی با 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 را صدا زد.
بررسی Request و Request Handler در MediatR
در MediatR برای رسیدگی به Request ها از Handler ها استفاده میکنیم. هر درخواست از اینترفیس IRequest (و یا مدل Generic آن ) ارث بری میکند. و Handler مربوط به آن از اینترفیس IRequestHandler<TRequest,TResponse ارث بری میکند. که TRequest همان درخواست می باشد که از IRequest ارث بری کرده است و به این ترتیب Request به Handler خود مپ میشود. در نمونه کد زیر یک Request و Request Handler مربوط به آن را میتوانید ببینید.
در مدل جنریک Handler، خروجی درخواست معلوم میشود که از جنس boolean می باشد. اینترفیس IRequestHandler یک متد به اسم Handle دارد که باید پیاده سازی شود.
سپس در کنترلر مربوطه، یک instance از IMediator را تزریق کرده و کلاس Request را به شکل زیر به آن پاس میدهیم.
استفاده از 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 را تزریق میکنیم.
به AddMediaR باید Assembly مربوط به قسمتی که Handler ها در آن قرار دارد را پاس بدهیم. در این پروژه چون Class Library جدا نداریم همان Assembly مربوط به Startup را پاس دادیم. تمامی تنظیمات مربوط به MediatR انجام شده است.
ایجاد اولین Request و Handler
برروی سولوشن راست کلیک کرده یک فولدر به نام Data ایجاد میکنیم. در این پوشه، پوشه دیگری به نام Model ایجاد میکنیم که در این پوشه قرار است مدل ها و انتیتی های پروژه قرار بگیرند.
در پوشه Model یک کلاس به نام Book ایجاد میکنیم
سپس در پوشه Model یک کلاس به نام Book ایجاد میکنیم که شامل Property های زیر می باشد.
این کلاس قرار است نقش Book Entity را بازی کند.
در پوشه Data، یک پوشه دیگر تحت عنوان Repositories می سازیم.سپس در پوشه Repository یک پوشه دیگر تحت عنوان BookRepository می سازیم که قرار است در این پوشه ریپوزیتوری مربوط به کلاس Book را بسازیم.
در این پروژه از In Memory Database استفاده میکنیم و لیستی از Book را درون حافظه ذخیره میکنیم و از آن نیز Book را باز خوانی میکنیم.
یک اینترفیس با نام IBookRepository می سازیم.
سپس کلاسی با نام BookMockRepository می سازیم و این اینترفیس را در آن به شکل زیر پیاده سازی میکنیم.
سپس این اینترفیس را به صورت Scoped رجیستر میکنیم.
حال همه چیز برای ایجاد اولین Handler آماده است. در پروژه یک پوشه دیگر به نام Application ایجاد میکنیم. لایه Application وظیفه سازماندهی به ریکوئست ها را بر عهده دارد ( Request Orchestration). برای مطالعه بیشتر درباره لایه اپلیکیشن و همچنین لایه بندی پروژه میتوانید به این لینک مراجعه کنید.
در پوشه Application یک پوشه به نام BookApplication ایجاد میکنیم. در این پوشه یک پوشه دیگر به نام GetBookById ایجاد میکنیم.در این پوشه دو کلاس قرار میگیرد:
- GetBookByIdRequest
- GetBookByRequestHandler
کلاس GetBookByIdRequest از اینترفیس IRequest ارث بری میکند.این اینترفیس جنریک یک پارامتر ورودی دارد که نوع Response یک Request را مشخص میکند. که در اینجا Response درخواست GetBookByIdRequest یک Book می باشد.
در قدم بعدی برای این Request یک Handler مینویسیم که وظیفه ایجاد Response برای GetBookByIdRequest را برعهده دارد. هر Handler از اینترفیس IRequestHandler ارث بری میکند. این اینترفیس دو پارامتر را به عنوان وروردی جنریک میگیرد، اولین پارامتر Request و دومین پارامتر Response مربوطه می باشد. این اینترفیس یک متد به نام Handle از جنس Task دارد که باید پیاده سازی شود.
در این Handler یک نمونه از IBookRepository را Inject میکنیم. در متد Handle با استفاده از BookRepository یک Instance از Book را به عنوان Response بر میگردانیم.
سپس یک کنترلر برای استفاده از این Handler می سازیم و نام آن را BookController قرار میدهیم.
برای استفاده از Handler های MediatR ، اینترفیس IMeditor را در کنترلر تزریق میکنیم.برای دریافت Book از طریق Id، ریکوئست GetBookByIdRequest را به متد Send در اینترفیس IMediator پاس میدهیم.حال این متد یک Book را به ما بر میگرداند چرا که در تایپ IRequest یک Book را به عنوان Response تعریف کرده ایم.
به وسیله Swagger این API را چک میکنیم. و جواب یک Instance از Book می باشد.
آشنایی با Behavior های MediatR
با استفاده از Behavior ها در MediatR میتوان پایپ لاین های مخصوص ساخت که در حین، قبل و یا بعد اجرای یک Handler اجرا شوند.
میخواهیم با استفاده از Behavior ، درخواست هایی که به سمت هرکدام از Handler ها فرستاده میشوند را چک کنیم و در صورت وجود خطا،آن را لاگ کنیم.
در پوشه Application یک پوشه دیگر به نام Common می سازیم. در این پوشه یک کلاس به نا LoggingBehavior ایجاد میکنیم.
این کلاس را به صورت جنریک در می آوریم. برای ساخت پایپ لاین در MediatR باید کلاس، اینترفیس IPiplelineBehavior ارث بری کند. این اینترفیس یک متد به نام Handle و از جنس Task دارد که باید پیاده سازی شود.
در قطعه کد بالا، یک اینستنس از ILogger را به پایپ لاین تزریق کردیم که در صورت وقوع خطا از آن برای لاگ کردن خطا استفاه میکنیم.
برای اینکه در ادامه Handler مربوطه صدا زده شود،نیاز است که next صدا زده شود. این عمل در اصل باعث تشکیل Pipleline میشود و باعث میشود که LoggingBehavior در مسیر یک درخواست برای رسیدن به یک Handler قرار بگیرد.
پس از ساخت Pipeline آن را در Startup رجیستر میکنیم.
برای تست پایپ لاین، در GetBookByIdRequestHandler یک خطای تستی پرتاب میکنیم.
و مجدد Api ای که ساختیم را در Swagger صدا میزنیم. مشاهده میکنیم و که Pipeline ساخته شده حین درخواست، خطا را Catch و آن را لاگ کرده است و API در واقع خطا را دریافت نکرده است
در MediatR دو Behavior به صورت پیش فرض وجود دارد مه به صورت پیش فرض در MediatR رجیستر شدند و میتوان آنها را پیاده سازی و استفاده کرد.
- IRequestPreProccessor
این پایپ لاین قبل از اجرای هر Handler اجرا میشود. این اینترفیس یک متد به نام Process دارد که نوع درخواست را در را میتوان به وسیله آن در اختیار داشت و با توجه به آن قبل از هر درخواست یک سری عملیات را انجام داد.
- IRequestPostProcessor
این پایپ لاین بعد از اجرای هر Handler اجرا میشود. این اینترفیس یک متد به نام Process دارد که به وسیله آن میتوان نوع درخواست و همچنین نوع Response را در اختیار داشت و با توجه به آنها یک سری عملیات انجام داد.
پیاده سازی الگوی Unit of Work توسط Post Processor در MediatR
همانطور که میدانید، Post Processor ها بعد از اجرای هر Handler اجرا میشوند و توسط آنها میتوان به نوع درخواست و همچنین نوع Response دسترسی داشت. در این قسمت توسط یک Post Processor میخواهیم الگوی Unit of Work را پیاده سازی کنیم. برای مطالعه درباره Unit of Work میتوانید به این لینک مراجعه کنید
در پوشه Common یک اینترفیس خالی با نام ICommitable ایجاد میکنیم. بوسیله این اینترفیس میخواهیم تمامی درخواست هایی که قرار است در دیتابیس تغییرات داشته باشند را علامتگذاری کنیم.
در پوشه BookApplication، یک پوشه جدید به نام AddBook ایجاد میکنیم. در این پوشه یک کلاس به نام AddBookRequest به شکل زیر ایجاد میکنیم. این کلاس از اینترفیس ICommitable ارث بری میکند و به نوعی آن را با اینترفیس ICommitable علامت گذاری میکنیم.
هندلر مربوط به این Request را به شکل زیر میسازیم.دقت کنید که برای اضافه کردن مدل جدید، از Mock Repository که قبلتر ساختیم استفاده میکنیم.
برای پیاده سازی الگوی Unit of Work به سراغ پیاده سازی Post Processor آن میرویم. در پوشه Common یک کلاس به نام CommitPostProcessor می سازیم. در اینجا برای نمونه در کنسول لاگ ثبت میکنیم و به نوعی ثبت داده در دیتابیس را شبیه سازی میکنیم.
همانطور که میدانید Post Processor ها در انتهای هر Request به MediatR اجرا میشوند، پس در اینجا در انتهای هر Request چک میکنیم که آیا Request با ICommitable علامت گذاری شده است یا خیر و اگر علامت گذاری شده بود تغییرات را در دیتابیس اعمال میکنیم. به طور مثال اگر از EF Core استفاده میکنید میتوانید در اینجا متد Save Changes روی DbContext را صدا بزنید.(برای مشاهده منبع اصلی این قسمت میتوانید به این لینک در گیت هاب مراجعه کنید.)
در نهایت API Action مربوطه را ساخته و با استفاده از اینترفیس IMediator این درخواست را صدا میزنیم.
بررسی الگوی 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 را به شکل زیر ایجاد میکنیم.
دقت کنید که این کلاس از INotification ارث بری میکند و شامل دو پراپرتی Email Address و Email Content می باشد که باید مقدار دهی شوند.
سپس کلاس EmailNotificationHandler را به شکل زیر پیاده سازی میکنیم. برای شبیه سازی، به جای ارسال ایمیل، یک لاگ در کنسول ثبت میکنیم.
به BookController بر میگردیم. در اکشن CreateBook برای استفاده از این Notification از متد Publish اینترفیس IMediator استفاده میکنیم.
در خط 8 کد بالا، Notification مربوط به ارسال Email را پابلیش کرده ایم. در کنسول خواهیم داشت:
مشاهده میشود که Event مربوطه به ارسال Notification توسط EmailNotifcation Handler اجرا شده است.
نتیجه گیری
در این قسمت به بررسی MediatR پرداختیم و با ویژگی های آن آشنا شدیم. پیاده سازی الگوی Mediator با استفاده از کتابخانه MeidatR کاری بسیار راحت و بی دردسر است که به ارائه سولوشن تمیز و ایجاد Loose Coupling (که از ملزومات پیاده سازی CQRS می باشد) کمک بسیازی میکند. همچنین در MediatR میتوان Pipeline های اختصاصی ایجاد کرد و با استفاده از Post Processor ها الگوی Unit of Work را پیاده سازی کرد. در قسمت بعد به پیاده سازی پروژه به روش CQRS با استفاده از MediatR می پردازیم.
اگر به سورس کد این قسمت نیاز داشتید میتوانید آن را لینک زیر در گیت هاب دریافت نمایید. و همچنین اگر سوال یا مطلبی بود، خوشحال میشوم که آن را در بخش نظرات مطرح فرمایید.
مقالات بیشتر در دات نت زوم
مطلبی دیگر از این انتشارات
آموزش Microservices در ASP.NET Core (سری اول)
مطلبی دیگر از این انتشارات
کتابخانه ای جهت پیاده سازی Unobtrusive Ajax در ASP.NET Core
مطلبی دیگر از این انتشارات
آموزش Unit Testing با استفاده از NUnit و Moq بخش دوم: Mocking