بررسی عملی CQRS- بخش سوم: پروژه عملی



در این بخش به بررسی و پیاده سازی یک پروژه عملی ساده با CQRS و SQL Server و Mongo DB می پردازیم




در این پروژه قرار است با دو دیتابیس کار کنیم. ابتدا توسط یک Command دیتا را با EF Core در دیتابیس SQL Server ذخیره میکنیم و سپس یک Event ایجاد میکنیم. این Event توسط یک Background Service دریافت شده و دیتا را داخل Mongo DB ذخیره میکند. سپس هرجا که نیاز به خواندن و واکشی داده داشتیم، از داده موجود در Mongo DB استفاده میکنیم.


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

https://vrgl.ir/01pUA


https://vrgl.ir/0a7QA

آشنایی با 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 استفاده کنیم.


داشبورد کلی Mongo Compass
داشبورد کلی 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 استفاده کنیم.

معماری استاندارد 3-tier
معماری استاندارد 3-tier


نصب پکیج های مورد نیاز

برای کار با 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 تعیین میکنیم که فیلد به چه صورت در دیتابیس ذخیره شود.

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

برای Write Model ها دو موجودیت Movie و Director را در نظر میگیریم که هر Director میتواند چند Movie داشته باشد.

https://gist.github.com/babaktaremi/e73b96652028bcc75f604aa461a48097
https://gist.github.com/babaktaremi/c54d053afd34dd8d85d1353477a2a791

سپس به سراغ ساخت ApplicationDbContext می رویم و آن را به صورت زیر می سازیم.


https://gist.github.com/babaktaremi/839ab549214f396a9cdf5abd6f72f769

در کلاس Startup باید ApplicationDbContext ای که ساختیم را رجیستر کنیم

https://gist.github.com/babaktaremi/9df59b7914604bf67ebdbcdac6b53edc

پس از این کار، به Package Manger Console مراجعه کرده و اولین Migration را برای ساخت جداول اعمال میکنیم.


ساخت ریپوزیتوری های مربوط به Write Model

پس از ساخت دیتابیس به سراغ ساخت Repository های مربوط به آن میرویم. در لایه Data Access یک پوشه با نام WriteRepositories می سازیم و سپس در آن دو کلاس با نام های DirectorRepository و WriteMovieRepository می سازیم.

در کلاس WriteMovieRepository سه متد با نام های AddMovieAsync ، GetMovieByIdAsync و DeleteMovie به صورت زیر خواهیم داشت

https://gist.github.com/babaktaremi/8271295364b77b06d38acefdacfc6f28


برای DirectorRepository دو متد با نام های GetDirectorAsync و AddDirectorAsync به صورت زیر خواهیم داشت.

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

سپس این دو Repository را در Startup رجیستر میکنیم

https://gist.github.com/babaktaremi/5caa4f7ced744373fd89b94c7fe9a080


ساخت ریپوزیتوری های مربوط به Read Model

در لایه Data Access یک پوشه با نام ReadRepositories می سازیم و سپس در آن یک پوشه دیگر با نام Common می سازیم. در پوشه Common برای راحتی کار با Mongo Driver اقدام به ساخت یک BaseReadRepository می کنیم که در آن از متد های موجود در Mongo Driver استفاده میکنیم

کلاس BaseReadRepository را به صورت زیر می سازیم.

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


یکی از Best Practice ها در مورد Mongo Driver آن است که باید به صورت Singleton در سیستم رجیستر شود چرا که ساخت هرباره یک Instance از Mongo Client بسیار هزینه بر است. پس Mongo Database را به شکل زیر به صورت Singleton رجیستر میکنیم. دقت کنید که در اینجا Connection string و دیتابیس مربوطه در Mongo را هم به Mongo Client پاس میدهیم. یکی از مزیت های Mongo DB آن است که برای ساخت Collection ها نیازی به Migration نیست و به صورت خودکار ساخته می شوند.


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


سپس ReadMovieRespository را به شکل زیر می سازیم و آن را رجیستر میکنیم.

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


https://gist.github.com/babaktaremi/52f79f31e93f248508479466b18b4023

آشنایی با 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 را به شکل زیر ایجاد میکنیم.

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

سپس این چنل جنریک را به صورت Singleton در سیستم رجیستر میکنیم

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


مسئله Eventual Consistency

زمانی که عملیات Write در دیتابیس اصلی اتفاق می افتد، این تغییر با یک تاخیر در دیتابیس های Read اعمال می شود.

در این حالت چون تغییرات به صورت Eventually ( نه در لحظه، بلکه با تاخیر و در نهایت) Sync میشوند، ممکن است داده ای که در لحظه از دیتابیس Read واکشی می شود، به روز و آخرین نسخه نباشد.

این تکنیک در سیستم های توزیع شده باعث می شود که در عوض یکپارچگی و ثبات داده ها (Consistency) به پرفورمنس بهتر، Scalability و High Availability دست پیدا کنیم

ساخت اولین Command

ابتدا به سرغ ساخت AddMovieCommand می رویم. این Command قرار است توسط Handler مربوطه، داده را هم در دیتابیس SQL Server و هم در Mongo DB ذخیره کند.در لایه Core یک پوشه به نام MovieApplication می سازیم و در آن یک پوشه دیگر به نام Commands می سازیم و در آن یک پوشه دیگر به نام AddMovie می سازیم.



https://gist.github.com/babaktaremi/fb934879479d9552389c3e6eabaa3c8c
https://gist.github.com/babaktaremi/712a08964962bd2f4f5a602ac3621aec


سپس به سراغ ساخت Command Handler مربوط به AddMovieCommand می رویم.

https://gist.github.com/babaktaremi/570340969736a70326ee39ae71c13ba8

در خط 6 یک نمونه از چنل را Inject کرده ایم که قرار است Message این چنل توسط یک Background Service مدیریت شود و Movie توسط آن به دیتابیس Mongo اضافه شود. مدل Message قرار داده شده در چنل به شکل زیر است. ابتدا در پوشه MovieApplication یک پوشه با نام BackgroundWorker ایجاد میکنیم و سپس در آن یک پوشه دیگر با نام Common ایجاد میکنیم و در آن یک پوشه دیگر با نام Events ایجاد میکنیم

در این پوشه یک کلاس با نام MovieAdded به شکل زیر ایجاد میکنیم

https://gist.github.com/babaktaremi/424c3df4ed471e4eff5a42dc107400c4

برای ساخت Background Worker مربوطه یک پوشه با نام AddReadMovie ایجاد میکنیم و در آن یک کلاس با نام AddReadModelWorker می سازیم.

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

چون Background Service ها به صورت Singleton در سیستم رجیستر میشوند، به کمک IServiceProvider یک Scope می سازیم و در آن سرویس های مورد نیاز را Inject میکنیم. در خط 19 و 20 به ترتیب WriteMovieRepository و ReadMovieRepository را از Scope ساخته شده دریافت میکنیم. سپس Movie را از دیتابیس دریافت کرده و در دیتابیس Mongo قرار میدهیم.

در نهایت این Background Service را در Startup رجیستر میکنیم.

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

به همین ترتیب میتوان سرویس های مربوط به Delete و Update را نیز نوشت.


ساخت کوئری و دریافت Movie از Mongo DB

در پوشه MovieApplication یک پوشه دیگر با نام Queries می سازیم و در آن یک پوشه با نام GetMovieById می سازیم. در این پوشه دو کلاس با نام های GetMovieByNameQuery و GetMovieByNameQueryHandler را قرار میدهیم.


https://gist.github.com/babaktaremi/50b4894e0033511829835f35062ed772

اگر یادتان باشد قبلتر در این مقاله در مورد ReadMovieRepository صحبت کردیم و در آن متدی با نام GetByNameAsync داشتیم که یک Movie را از دیتابیس Mongo بر اساس نام فیلتر و واکشی میکند. از این متد در Handler مربوط به این کوئری استفاده خواهیم کرد.

https://gist.github.com/babaktaremi/981ecfa03014e4a17d8f3a6a16e13b2b


ساخت کنترلر Movie

ابتدا یک Instance از IMediator را در کنترلر Inject میکنیم. سپس اکشن AddMovie را به شکل زیر می سازیم.

https://gist.github.com/babaktaremi/7be3b102908fe5104d8133cc98d5e3a6

سپس بوسیله Swagger این اکشن را تست میکنیم.

به SQL Server Management Studio مراجعه میکنیم. ملاحظه میشود که یک Instance از Movie و یک Instance از Director به دیتابیس اضافه شده است.

حال به قسمت مهم ماجرا میرسیم. به سراغ Mongo DB Compass می رویم. در ابتدا یک دیتابیس با نام moviesdatabase ساختیم. به سراغ آن میرویم. مشاهده می شود که یک Collection با نام Movie ساخته شده است که در آن اطلاعات Movie به صورت Json قرار دارد.

ساخت اکشن GetMovie

همانند اکشن AddMovie این بار با استفاده از MediatR به سراغ واکشی اطلاعات یک Movie می رویم.

https://gist.github.com/babaktaremi/0969747336d50c553ad008e569160a63

برای تست این اکشن به سراغ Swagger می رویم. ملاحظه میشود که اطلاعات از دیتابیس Mongo DB خوانده میشود چرا که یک ObjectId که Mongo به Document اختصاص میدهد نیز واکشی شده است.

نتیجه گیری

در این مقاله یک پروژه بسیار ساده را با استفاده از MediatR و دیتابیس های MongoDB و SQL Server با پترن CQRS پیاده سازی کردیم. توصیه میکنم که این پروژه را از گیت هاب دریافت کرده و با همین پترن، اکشن Update یک Movie را پیاده سازی کنید. از طریق لینک زیر میتوانید پروژه را از گیت هاب دریافت کنید و اگر سوال و یا پیشنهادی دارید، خوشحال میشوم که آن را در قسمت نظرات مطرح کنید.


https://github.com/babaktaremi/Sample-CQRS-Project


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

https://t.me/DotNetZoom