بررسی عملی 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بر روی دکمه 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نصب پکیج های مورد نیازبرای کار با 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