الگو یا کتابخانه MediatR برای کاهش وابستگی ها بین object ها استفاده میشوند، بهطور کلی این کتابخانه برای این توسعه داده شده است تا دو الگوی معماری نرمافزار یعنی CQRS و الگوی Mediator را ساده کند.
بیایید ابتدا نگاهی به این دو الگو بیندازیم.
این الگو به معنای جدا سازی مسئولیت های نوشتن و خواندن است (Command Query Responsibility Segregation)، بهطور کلی هدف این الگو جدا سازی عملیات های خواندن و نوشتن به دو مدل متفاوت است.
اگر به الگوی های CRUD دقت کنیم، عموما ما واسطهای کاربری داریم که مسئول انجام دادن این عملیاتها بر روی یک پایگاه داده هستند، به عبارت دیگر تمامی این عملیات توسط یک واسط صورت میگیرد.
اما اجازه دهید یک سؤال مطذح کنم، بین تمامی این 4 عملیات کدام عملیات بیشتر در طول عمر یک نرمافزار مورد استفاده قرار میگیرد؟
الگوی CQRS این عنلیات ها را به دو مدل جدا میکند، یک عملیات برای Query ها که به عنوان R شناسایی میشود، و دیگیری برای دستورها که با CUD شناسایی میشود.
شکل زیر نمایش می دهد که این الگو چگونه مورد استفاده قرار میگیرد.
همانطور که مشاهده می کنید، نرمافزار ما به سادگی به دو مدل Query و Command تقسیم شده است.
این الگو به سادگی توضیف میکند که یک object کپسوله شده است چگونه با دیگر object ها تعامل برقرار میکند. به عبارت دیگر، به جای اینکه دو یا چند object که به هم وابستگی داشته باشند، تنها یک واسطه مسئول برقراری این ارتباط و تعاملات خواهد بود، که به این واسطه Mediator گفته می شود.
همانطور که در شکل بالا مشاهده میکنید، سرویس های ما یک پیام را به Mediator ارسال میکنند، و سپس Mediator آنها را از Handler ها تقاضا میکند.در این الگو هیچ وابستگی بین سرویس ها و Handler ها وجود ندارد و تعامل این دو با استفاده از Mediator صورت میگیرد.
دلیل اینکه الگوی Mediator کاربرد موثری دارد این است که از الگوی inversion of control پیروی میکند که خود باعث فعال سازی loose coupling خواهد شد، بدین ترتیب وابستگی ها کاهش میبایند و کدها و تست های شما را سادهتر خواهد کرد.
در تصویر قبلی دیدیم که چگونه سرویسها هیچ وابستگی مستقیمی ندارند و تولیدکننده پیامها نمیداند چه کسی یا چند چیز قرار است آن را مدیریت کند. این الگو بسیار شبیه به نحوه عملکرد یک واسطه پیام در الگوی "publish/subscribe" است. بدین ترتیب که اگر نیاز باشد تا یک Handler دیگر اضافه کنیم بدون اینکه تغییری در سرویس ها داشته باشیم.
اکنون که این دو مدل را بررسی کردیم، نوبت آن است که با استفاده از کتابخانه MediatR این دو مدل را توسعه دهیم.
همانطور که قبلا گفتم الگو یا کتابخانه MediatR برای کاهش وابستگی ها بین object ها استفاده میشوند.
برای توسعه این کتابخانه نیاز به یک برنامه ASP.NET Core API داریم، برای دسترسی به کدهای این مقاله میتوانید آن را از Github من استفاده کنید.
ابتدا نیاز داریم تا وابستگی های مربوط به برنامه را از طریق Nuget یا Package Manager Console نصب کنیم:
PM> install-package MediatR
در نسخه های قبل نیاز بود تا برای تزریق وابستگی ها پکیج MediatR.Extensions.Microsoft.DependencyInjection
را نیز نصب کنیم، اما در نسخه 12 به بعد نیازی به نصب این پکیج نیست.
بعد از نصب این پکیج، فایل csproj. شما چیزی شبیه این باید باشد:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net7.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="MediatR" Version="12.1.1" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.7"/> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/> </ItemGroup> </Project>
اجازه دهید تا فایل lunchSettings را نیز بروز رسانی کنیم:
{ "profiles": { "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
حال که پکیج خود را نصب کردیم، نیاز است تا تزریق وابستگی مروبط به سرویس MediatR را ثبت کنیم، برای این کار در فایل program.cs از کد زیر استفاده کنید:
builder.Services .AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
بعد از ثبت سرویس مروبط به MediatR اجازه دهید کنترلر جدیدی را اضافه کنیم و از MediatR استفاده کنیم.
در پوشه مربوط به Controllers یک کنترلر جدید به نام BookController اضافه کنید، و کدهای زیر را به آن اضافه کنید.
private readonly IMediator _mediator; public BookController(IMediator mediator) => _mediator = mediator;
در نهایت BookController شما باید شبیه به کد زیر باید:
using MediatR; using Microsoft.AspNetCore.Mvc; namespace MP.API.Controllers; [ApiController] [Route("[controller]")] public class BookController:ControllerBase { private readonly IMediator _mediator; public BookController(IMediator mediator) => _mediator = mediator; }
واسط IMediatR به شما این اجازه را میدهد تا پیام های خود را به MediatR ارسال کنیم، که در ادامه به Handler مروبطه ارسال خواهد شد.
در دنیای واقعی برای ذخیره اطلاعا ما به پایگاه داده نیاز داریم، اما در این مقاله برای تسهیل درک این الگو از یک fake class استفاده کرده ام.
کلاس Book
public class Book { public long Id { get; init; } public string Name { get; init; } }
این کلاس قطعا پراپرتی ها بیشتری نسبت به کد بالا دارد، اما برای سادهسازی تنها به Id و نام کلاس بسنده کرده ام.
حال بیایید fake data store را اضافه کنیم.
public class FakeDataStore { private static readonly List<Book> Books = new() { new Book {Id = 1,Name="C#"}, new Book {Id = 2,Name="ASP.NET core"}, new Book {Id = 3,Name="SQL Server"}, }; public async Task<Book> AddBook(Book book) { Books.Add(book); return await Task.FromResult(book); } public async Task<IEnumerable<Book>> GetAllBooks() =>await Task.FromResult(Books); }
بعد از افزودن fake data store نوبت به آن میرسد که آن را در pipleline اضافه کنیم، بدین ترتیب در فایل program.cs کد زیر را اضافه می کنیم:
builder.Services.AddSingleton<FakeDataStore>();
تا به اینجا datastore ما نیز توسعه داده شده است، حال نوبت آن است تا از الگوی CQRS استفاده کنیم.
همانطور که قبلا گفتم، هدف CQRS جداسازی خواندن و نوشتن است، بدین منظور باید در برنامه خود این عملیا را جدا کنیم. بدین ترتیب نیاز دارین تا دو پوشه به نام های Commands و Queries به پروژه خود اضافه کنیم.
در شکل بالا بهصورت ضمنی فرایند های خواندن و نوشتن را جدا کردیم، حال نوبت آن است تا کدها و Handlerمربوط به آنها را پیادهسازی کنیم.
ابتدا اجازه دهید تا از Queries شروع کنیم، با مراجه به Fake data store در میابیم که یک متد به نام GetAllBooks اضافه کرده ایم، بدین منظور برای تکمیل فرایند Query نیاز به یک فایل record و یک کلاس داریم.
بدین ترتیب یک فایل record به نام GetAllBooksQuery اضافه میکنیم و کد زیر را نیز به آن اضافه میکنیم.
public record GetAllBooksQuery():IRequest<IEnumerable<Book>>;
بدین ترتیب ما یک رکورد به نام GetAllBooksQuery اضافه کردیم که از IRequest ارث بری میکند و مجموعه ای از کتابها را درخواست میکند.
توجه داشته باشید، با این کد ما فقط پیام مربوط به درخواست را توسعه داده ایم اما هنوز Handler مربوط به این درخواست را توسعه نداده این، به عبارتی دیگر یک Handler باید ایجاد کنیم که مسئول رسیدگی به درخواست ما باشد و سپس اطاعات مورد نیاز را از Data store واکشی و سپس از طریق MediatR به Controller باز گرداند.
پس اجازه بیایید Handler مربوط به این درخواست را نیز اضافه کنیم، بدین ترتیب یک کلاس به نام GetAllBooksQueryHanlder اضافه کنیم و کدهای زیر را نیز اضافه میکنیم.
public class GetAllBooksQueryHandler : IRequestHandler<GetAllBooksQuery, IEnumerable<Book>> { private readonly FakeDataStore _fakeDataStore; public GetAllBooksQueryHandler(FakeDataStore fakeDataStore) { _fakeDataStore = fakeDataStore; } public async Task<IEnumerable<Book>> Handle(GetAllBooksQuery request, CancellationToken cancellationToken) => await _fakeDataStore.GetAllBooks(); }
بیایید به این کلاس بیندازیم، همانطور که مشاهده میکنید IRequestHandler که از نوع generic میباشد دارای دو پارامتر میباشد، پارامتر اول درخواستی که باید پردازش شود و پارامتر دوم پاسخ مربوط به handler میباشد.
در نهایت متد Handler درخواست را به سرویس fake data source میدهد و تمامی کتابها را در غالب یک مجموعه باز میگرداند.
قبل از اینکه به سراغ controller برویم اجازه دهید تا command مربوطه را نیز توسعه دهیم، برای این منظور مانند عملیات Query نیاز به یک رکورد و همچنین یک کلاس داریم.
بدین ترتیب یک فایل record به نام AddBookCommand اضافه میکنیم و کد زیر را نیز به آن اضافه میکنیم.
public record AddBookCommand(long Id,string Name):IRequest<Book>;
قبل از توسعه Hanlder مربوط به این رکورد نیاز میدانم دو مورد را توضیح دهم:
1- اگر دقت کرده باشی انتهای درکورد های تعریف شده با Query و Command تفیکیک شده اند، دلیل اینکار درک بهتر این درخواست است.
2- شاید سؤال پیش بیاید که دلیل بازگرداندن دوباره Book در این درخواست چیست؟ در دنیای واقعی شما نیازی به ارسال Id ندارید و در پشت صحنه ORM شما وظیفه تولید Id به عهده میگیرد، بدین ترتیب گاهی ممکن است شما نیاز به بازگرداندن Id و دیگر پارامترهای مربوط به کلاس باشید که در این صورت میتوانید از DTO ها برای انجام این کار استفاده کنید.
حال به سراغ Handler مربوط به Command برویم و آن را توسعه دهیم.
public class AddBookCommandHandler : IRequestHandler<AddBookCommand, Book> { private readonly FakeDataStore _fakeDataStore; public AddBookCommandHandler(FakeDataStore fakeDataStore) { _fakeDataStore = fakeDataStore; } public async Task<Book> Handle(AddBookCommand request, CancellationToken cancellationToken) { var book = new Book() {Id = request.Id,Name = request.Name}; return await _fakeDataStore.AddBook(book); } }
پس از توسعه Command و Query باید سرویس های مربوط به book شکل زیر باشند:
قدم آخر، توسعه Controller مربوط به Book میباشد و در نهایت ارسال پیام با استفاده از MediatR و پردازش آن توسط handler، بدین ترتیب کدهای مربوط به BookController را اضافه میکنیم:
[ApiController] [Route("[controller]")] public class BookController : ControllerBase { private readonly IMediator _mediator; public BookController(IMediator mediator) => _mediator = mediator; [HttpGet] [Route("GetAllBooks")] public async Task<IActionResult> GetAllBooks() { var response=await _mediator.Send(new GetAllBooksQuery()); return Ok(response); } [HttpPost] [Route("AddBook")] public async Task<IActionResult> GetAllBooks([FromBody] Book book) { var response=await _mediator.Send(new AddBookCommand(book.Id,book.Name)); return Ok(response); } }
حال برنامه را اجرا میکنیم و با استفاده از Postman، میتوانیم Endpoint خود را تست کنیم.
همانطور که در ابتدای مقاله فایل lunchSettings را ویرایش کردیم، برنامه ما در https://localhost:5001 اجرا خواهد شد، بدین ترتیب برای تست GetAllBooks باید Url ما به شکل زیر باشد.
https://localhost:5001/Book/GetAllBooks
در نهایت خروجی مربوط به این endpoint به شکل زیر خواهد بود:
حال زمان آن رسیده است تا یک کتاب به data store خود اضافه کنیم، بدین منظور باید یک درخواست به شکل زیر ارسال کنیم:
بعد از ارسال درخواست، دوباره GetAllBooks را فراخوانی میکنیم تا مطمئن شویم کتاب جدید اضافه شده باشد:
در این مقاله، در خصوص نحوه استفاده از MediatR با استفاده از الگوی CQRS و ASP.NET Core API صحبت کردم، متوجه شدیم که درخواست و Handler ها به چه صورتی انجام می شوند.
امیدوارم که از این مقاله لذت برده باشید.