C# enthusiast. NET foundation member
بررسی عملی CQRS- بخش سوم: پروژه عملی
در این بخش به بررسی و پیاده سازی یک پروژه عملی ساده با CQRS و SQL Server و Mongo DB می پردازیم
در این پروژه قرار است با دو دیتابیس کار کنیم. ابتدا توسط یک Command دیتا را با EF Core در دیتابیس SQL Server ذخیره میکنیم و سپس یک Event ایجاد میکنیم. این Event توسط یک Background Service دریافت شده و دیتا را داخل Mongo DB ذخیره میکند. سپس هرجا که نیاز به خواندن و واکشی داده داشتیم، از داده موجود در Mongo DB استفاده میکنیم.
در این مقاله از Github Gist استفاده شده است و لود شدن بخش مربوط به کد ها ممکن است کمی زمانبر باشد.
آشنایی با MongoDB
یک دیتابیس NoSQL می باشد که داده را به جای اینکه به صورت جدولی و با ردیف های مختلف ثبت کند، آنها را در کالکشن هایی به صورت document ذخیره میکند. در Mongo DB نیازی به تعریف جدول و روابط و ردیف های آن نیست. به همین علت می توان از آن برای Read Model استفاده کرد، چرا که هم پرفورمنس بهتری موقع کوئری گرفتن داده دارد و هم نیازی به تعریف روابط و عمل Join میان جداول مختلف نیست و میتوانیم داده را به صورت DeNormalized در آن ذخیره کنیم.
نصب و راه اندازی MongoDB در ویندوز
نصب Mongo روی ویندوز کار سر راست و ساده ای است. ابتدا به این لینک مراجعه کرده و نسخه Community آن را دانلود میکنیم. پس از دانلود و نصب آن Mongo Compass هم همراه با آن نصب میشود. Mongo Compass یک GUI برای مدیریت Mongo DB و داکیومنت ها و کالکشن های آن می باشد.
برای آشنایی بیشتر با Mongo Compass می توانید به این لینک مراجعه کنید.
پس از نصب، داشبورد Mongo Compass به طور خودکار بالا می آید. روی دکمه Connect کلیک کرده و هم اکنون میتوانیم از Mongo Compass استفاده کنیم.
بر روی دکمه Create Database کلیک کرده و یک دیتابیس درست میکنیم. نام هر دو Database Name و Collection Name را برای پروژه روی moviesdatabase قرار میدهیم و سپس بر روی دکمه Click Database کلیک میکنیم.
آشنایی با معماری کلی پروژه
پروژه دارای یک معماری استاندارد سه لایه (Data Access Layer-Core-Presentation) می باشد. در لایه Core جایی است که الگوی Mediator را به وسیله کتاب خانه MediatR پیاده سازی میکنیم و در لایه Data Access لایه برقراری با دیتابیس ها ( Mongo Db و SQL Server) می باشد. لایه Web جایی است که در آن API های پروژه قرار دارند و با استفاده از الگوی Dependecy Injection می توانیم از Handler های موجود در لایه Core استفاده کنیم.
نصب پکیج های مورد نیاز
برای کار با Entity Framework Core نیاز به پکیج های متداول زیر داریم. این پکیج ها را در لایه Data Access نصب میکنیم
Install-Package Microsoft.EntityFrameworkCore -Version 5.0.3
Install-Package Microsoft.EntityFrameworkCore.Abstractions -Version 5.0.3
Install-Package Microsoft.EntityFrameworkCore.Design -Version 5.0.3
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 5.0.3
Install-Package Microsoft.EntityFrameworkCore.Tools -Version 5.0.3
آشنایی با Mongo Driver: یک کتابخانه مانند Entity Framework می باشد که از آن میتوانیم برای ارتباط با Mongo DB استفاده کنیم و به نوعی یک Abstraction روی دستورات مورد نیاز برای کوئری و ثبت داده روی Mongo DB به همراه دارد.( در این مقاله به طور عمیق به Mongo Driver نمی پردازیم و در حد ثبت داده و اجرای یک کوئری ساده به آن بسنده میکنیم. برای آشنایی بیشتر با Mongo Driver میتوانید به مستندات آن مراجعه کنید.)
پکبج Mongo Driver را از طریق دستور زیر نصب میکنیم
Install-Package MongoDB.Driver -Version 2.11.6
در لایه Core نیاز به دو پکیج زیر برای استفاده MediatR داریم
Install-Package MediatR -Version 9.0.0
Install-Package MediatR.Extensions.Microsoft.DependencyInjection -Version 9.0.0
ساخت مدل های دیتابیس
همانطور که میدانید در CQRS مدل های Read و Write از یکدیگر جدا هستند. به همین منظور برای یک موجودیت نیاز به ساخت دو مدل هستیم. برای پروژه دمو یک موجودیت به نام Movie در نظر گرفته شده است که برای مدل Read و Write آن به شکل زیر عمل میکنیم. یکی از مزایای جداسازی مدل Read و Write آن است که می توانیم در مدل Read دیتا را به صورت Denormalized در بیاوریم و آن را ذخیره کنیم که برای واکشی اطلاعات دیگر نیازی به Join نباشد.
در مدل Read با صفت BsonId ابتدا Id دیتا را تعیین و مدل ذخیره سازی آن را تعیین میکنیم. سپس با صفت BsonElement تعیین میکنیم که فیلد به چه صورت در دیتابیس ذخیره شود.
برای Write Model ها دو موجودیت Movie و Director را در نظر میگیریم که هر Director میتواند چند Movie داشته باشد.
سپس به سراغ ساخت ApplicationDbContext می رویم و آن را به صورت زیر می سازیم.
در کلاس Startup باید ApplicationDbContext ای که ساختیم را رجیستر کنیم
پس از این کار، به Package Manger Console مراجعه کرده و اولین Migration را برای ساخت جداول اعمال میکنیم.
ساخت ریپوزیتوری های مربوط به Write Model
پس از ساخت دیتابیس به سراغ ساخت Repository های مربوط به آن میرویم. در لایه Data Access یک پوشه با نام WriteRepositories می سازیم و سپس در آن دو کلاس با نام های DirectorRepository و WriteMovieRepository می سازیم.
در کلاس WriteMovieRepository سه متد با نام های AddMovieAsync ، GetMovieByIdAsync و DeleteMovie به صورت زیر خواهیم داشت
برای DirectorRepository دو متد با نام های GetDirectorAsync و AddDirectorAsync به صورت زیر خواهیم داشت.
سپس این دو Repository را در Startup رجیستر میکنیم
ساخت ریپوزیتوری های مربوط به Read Model
در لایه Data Access یک پوشه با نام ReadRepositories می سازیم و سپس در آن یک پوشه دیگر با نام Common می سازیم. در پوشه Common برای راحتی کار با Mongo Driver اقدام به ساخت یک BaseReadRepository می کنیم که در آن از متد های موجود در Mongo Driver استفاده میکنیم
کلاس BaseReadRepository را به صورت زیر می سازیم.
یکی از Best Practice ها در مورد Mongo Driver آن است که باید به صورت Singleton در سیستم رجیستر شود چرا که ساخت هرباره یک Instance از Mongo Client بسیار هزینه بر است. پس Mongo Database را به شکل زیر به صورت Singleton رجیستر میکنیم. دقت کنید که در اینجا Connection string و دیتابیس مربوطه در Mongo را هم به Mongo Client پاس میدهیم. یکی از مزیت های Mongo DB آن است که برای ساخت Collection ها نیازی به Migration نیست و به صورت خودکار ساخته می شوند.
سپس ReadMovieRespository را به شکل زیر می سازیم و آن را رجیستر میکنیم.
آشنایی با Background Service در ASP Net Core
در ASP Net Core قابلیتی به نام Background Task وجود دارد که به کمک آن میتوان از یک Thread جداگانه در Background برای اجرای کارهای زمانبر و یا به صورت Interval بهره برد. هنگام استفاده از Background Task ها باید نکاتی را رعایت کنیم از جمله:
برای مطالعه بیشتر درباره Background Service ها و همچنین Hosted Service ها میتوانید به این لینک مراجعه کنید.
برای استفاده از Background Service ها نیازمند پکیج زیر هستیم که آن را در لایه Core نصب میکنیم
Install-Package Microsoft.Extensions.Hosting.Abstractions -Version 5.0.0
مختصر توضیحی درباره الگوی Pub/Sub
در الگوی Pub/Sub دو موجودیت داریم. Publisher و Subscriber که در آن Publisher یک Event را انتشار میدهد و Subscriber های آن Event بسته به شرایط یک سری عملیات خاص را مدیریت و اجرا میکنند. برای اینکه یک Subscriber از Event با خبر باشد، حتما باید عضوی از آن Event باشد که Publisher آن را Invoke میکند
این الگو برای ایجاد Loose Coupling و همچنین در Distributed System ها بسیار کاربرد دارد.
آشنایی با channel ها در ASP Net Core
خیلی جاها نیاز هست که یک سری داده در یک صف نگهداری بشوند که بعدا بتوانیم توسط سرویسی آنها را پردازش کنیم
در مقیاس کوچک، به جای استفاده از Message Broker هایی مانند RabbitMq میتوانیم از Channel ها استفاده کنیم. دقت داشته باشید که چنل در مقایسه با Message Broker هایی مثل RabbitMq محدودیت هایی دارد مثلا از چنل ها فقط در یک Application میتوان استفاده کرد و قابلیت به اشتراک گذاشتن محتوای یک Channel میان چند Application وجود ندارد
چنل در واقع صفی هست که thread safe هست و میتواند یک یا چند prodcuer و consumer داشته باشد برای آشنایی بیشتر با Channel ها میتوانید به این لینک مراجعه کنید
ساخت یک چنل جنریک
برای استفاده از Channel ها در پروژه به سراغ ساخت یک Channel جنریک میرویم که بتوانیم از آن در چند جا استفاده کنیم. ابتدا در پروژه یک پوشه به نام Common ایجاد میکنیم و سپس در آن یک پوشه دیگر به نام BaseChannel ایجاد میکنیم. در این پوشه کلاس ChannelQueue را به شکل زیر ایجاد میکنیم.
سپس این چنل جنریک را به صورت Singleton در سیستم رجیستر میکنیم
مسئله Eventual Consistency
زمانی که عملیات Write در دیتابیس اصلی اتفاق می افتد، این تغییر با یک تاخیر در دیتابیس های Read اعمال می شود.
در این حالت چون تغییرات به صورت Eventually ( نه در لحظه، بلکه با تاخیر و در نهایت) Sync میشوند، ممکن است داده ای که در لحظه از دیتابیس Read واکشی می شود، به روز و آخرین نسخه نباشد.
این تکنیک در سیستم های توزیع شده باعث می شود که در عوض یکپارچگی و ثبات داده ها (Consistency) به پرفورمنس بهتر، Scalability و High Availability دست پیدا کنیم
ساخت اولین Command
ابتدا به سرغ ساخت AddMovieCommand می رویم. این Command قرار است توسط Handler مربوطه، داده را هم در دیتابیس SQL Server و هم در Mongo DB ذخیره کند.در لایه Core یک پوشه به نام MovieApplication می سازیم و در آن یک پوشه دیگر به نام Commands می سازیم و در آن یک پوشه دیگر به نام AddMovie می سازیم.
سپس به سراغ ساخت Command Handler مربوط به AddMovieCommand می رویم.
در خط 6 یک نمونه از چنل را Inject کرده ایم که قرار است Message این چنل توسط یک Background Service مدیریت شود و Movie توسط آن به دیتابیس Mongo اضافه شود. مدل Message قرار داده شده در چنل به شکل زیر است. ابتدا در پوشه MovieApplication یک پوشه با نام BackgroundWorker ایجاد میکنیم و سپس در آن یک پوشه دیگر با نام Common ایجاد میکنیم و در آن یک پوشه دیگر با نام Events ایجاد میکنیم
در این پوشه یک کلاس با نام MovieAdded به شکل زیر ایجاد میکنیم
برای ساخت Background Worker مربوطه یک پوشه با نام AddReadMovie ایجاد میکنیم و در آن یک کلاس با نام AddReadModelWorker می سازیم.
چون Background Service ها به صورت Singleton در سیستم رجیستر میشوند، به کمک IServiceProvider یک Scope می سازیم و در آن سرویس های مورد نیاز را Inject میکنیم. در خط 19 و 20 به ترتیب WriteMovieRepository و ReadMovieRepository را از Scope ساخته شده دریافت میکنیم. سپس Movie را از دیتابیس دریافت کرده و در دیتابیس Mongo قرار میدهیم.
در نهایت این Background Service را در Startup رجیستر میکنیم.
به همین ترتیب میتوان سرویس های مربوط به Delete و Update را نیز نوشت.
ساخت کوئری و دریافت Movie از Mongo DB
در پوشه MovieApplication یک پوشه دیگر با نام Queries می سازیم و در آن یک پوشه با نام GetMovieById می سازیم. در این پوشه دو کلاس با نام های GetMovieByNameQuery و GetMovieByNameQueryHandler را قرار میدهیم.
اگر یادتان باشد قبلتر در این مقاله در مورد ReadMovieRepository صحبت کردیم و در آن متدی با نام GetByNameAsync داشتیم که یک Movie را از دیتابیس Mongo بر اساس نام فیلتر و واکشی میکند. از این متد در Handler مربوط به این کوئری استفاده خواهیم کرد.
ساخت کنترلر Movie
ابتدا یک Instance از IMediator را در کنترلر Inject میکنیم. سپس اکشن AddMovie را به شکل زیر می سازیم.
سپس بوسیله Swagger این اکشن را تست میکنیم.
به SQL Server Management Studio مراجعه میکنیم. ملاحظه میشود که یک Instance از Movie و یک Instance از Director به دیتابیس اضافه شده است.
حال به قسمت مهم ماجرا میرسیم. به سراغ Mongo DB Compass می رویم. در ابتدا یک دیتابیس با نام moviesdatabase ساختیم. به سراغ آن میرویم. مشاهده می شود که یک Collection با نام Movie ساخته شده است که در آن اطلاعات Movie به صورت Json قرار دارد.
ساخت اکشن GetMovie
همانند اکشن AddMovie این بار با استفاده از MediatR به سراغ واکشی اطلاعات یک Movie می رویم.
برای تست این اکشن به سراغ Swagger می رویم. ملاحظه میشود که اطلاعات از دیتابیس Mongo DB خوانده میشود چرا که یک ObjectId که Mongo به Document اختصاص میدهد نیز واکشی شده است.
نتیجه گیری
در این مقاله یک پروژه بسیار ساده را با استفاده از MediatR و دیتابیس های MongoDB و SQL Server با پترن CQRS پیاده سازی کردیم. توصیه میکنم که این پروژه را از گیت هاب دریافت کرده و با همین پترن، اکشن Update یک Movie را پیاده سازی کنید. از طریق لینک زیر میتوانید پروژه را از گیت هاب دریافت کنید و اگر سوال و یا پیشنهادی دارید، خوشحال میشوم که آن را در قسمت نظرات مطرح کنید.
مقالات بیشتر در دات نت زوم
مطلبی دیگر از این انتشارات
مشاهده تاریخچه فعالیت های یک ریپازیتوری یا یک شخص در گیتهاب با GitMemory
مطلبی دیگر از این انتشارات
آموزش gRPC در ASP.NET Core - قسمت اول
مطلبی دیگر از این انتشارات
اهمیت Side-effect Free و Idempotency در کدنویسی