الگوی CQRS مخفف Command and Query Responsibility Segregation است، الگویی که عملیات خواندن و به روز رسانی را برای ذخیره داده جدا می کند. پیاده سازی CQRS در برنامه شما می تواند عملکرد، مقیاس پذیری و امنیت آن را به حداکثر برساند. انعطافپذیری ایجاد شده با مهاجرت به CQRS به سیستم اجازه میدهد در طول زمان بهتر تکامل یابد و از ایجاد تداخل merge conflicts در سطح دامنه توسط دستورات بهروزرسانی جلوگیری میکند.
در اینجا Command به یک فرمان پایگاه داده اشاره دارد که می تواند یک عملیات Insert / Update یا Delete باشد، در حالی که Query مخفف Querying data از یک منبع است. اساساً نگرانی ها را از نظر خواندن و نوشتن از هم جدا می کند، که بسیار منطقی است. این الگو از اصل جداسازی command و query ابداع شده توسط برتراند مایر نشات گرفته است. که بیان میکند که هر متد یا باید command ی باشد که یک عمل را انجام میدهد، یا کوئری است که دادهها را به تماسگیرنده برمیگرداند، اما نه هر دو.
مشکل الگوهای معماری سنتی این است که از همان مدل داده یا DTO برای پرس و جو و همچنین به روز رسانی Data source استفاده می شود. زمانی که برنامه شما فقط به عملیات CRUD مربوط می شود و نه چیزی بیشتر، این می تواند روشی مناسب باشد. اما زمانی که الزامات شما به طور ناگهانی شروع به پیچیده شدن می کند، این رویکرد اساسی می تواند فاجعه باشد.
حجم کار خواندن و نوشتن اغلب نامتقارن است، با عملکرد و مقیاس بسیار متفاوت.
ایده CQRS این است که به برنامه اجازه می دهد با مدل های مختلف کار کند. به طور خلاصه، شما یک مدل دارید که داده های مورد نیاز برای به روز رسانی یک رکورد، مدل دیگری برای درج یک رکورد، و مدلی دیگر برای پرس و جو یک رکورد را دارد. این به شما انعطاف پذیری با سناریوهای مختلف و پیچیده می دهد. با اجرای CQRS لازم نیست فقط به یک DTO برای کل عملیات CRUD تکیه کنید.
الگوی CQRS خواندن و نوشتن را در مدلهای مختلف جدا میکند و از commandها برای بهروزرسانی دادهها و از query برای خواندن دادهها استفاده میکند.
سپس میتوان مدلها را جدا کرد، همانطور که در نمودار زیر نشان داده شده است، اگرچه این یک الزام مطلق نیست.
داشتن مدل های پرس و جو و به روز رسانی جداگانه، طراحی و پیاده سازی را ساده می کند. با این حال، یک نقطه ضعف این است که کد CQRS نمی تواند به طور خودکار از یک database schema با استفاده از مکانیسم های scaffolding مانند ابزارهای O/RM تولید شود.
برای جداسازی بیشتر، می توانید داده های خوانده شده را از داده های نوشتن به صورت فیزیکی جدا کنید. در آن صورت، پایگاه داده خوانده شده می تواند از طرح داده های خود استفاده کند که برای پرس و جوها بهینه شده است. حتی ممکن است از نوع دیگری از ذخیره داده استفاده کند. به عنوان مثال، پایگاه داده write ممکن است رابطه ای باشد، در حالی که پایگاه داده read یک document database است.
اگر از پایگاههای اطلاعاتی جداگانه برای read و write استفاده میشود، باید آنها را هماهنگ(sync) نگه داشت. این معمولاً با انتشار یک رویداد توسط write model هر زمان که پایگاه داده را به روز می کند، انجام می شود.
به روز رسانی پایگاه داده و انتشار رویداد باید در یک تراکنش انجام شود.
حجم کار خواندن و نوشتن اغلب نامتقارن است، با عملکرد و مقیاس بسیار متفاوت.معمولاً حجم کار خواندن با بار بسیار بالاتری نسبت به نوشتن مواجه میشوند.
برخی از پیاده سازی های CQRS از الگوی Event Sourcing استفاده می کنند. با این الگو، وضعیت برنامه به عنوان یک توالی از رویدادها ذخیره می شود. هر رویداد نشان دهنده مجموعه ای از تغییرات در داده ها است. وضعیت فعلی با پخش مجدد رویدادها ساخته می شود. در زمینه CQRS، یکی از مزایای Event Sourcing این است که از همان رویدادها می توان برای اطلاع رسانی به اجزای دیگر استفاده کرد - به ویژه برای اطلاع رسانی به مدل خوانده شده. Read Model از رویدادها برای ایجاد یک snapshot از وضعیت فعلی استفاده می کند که برای پرس و جوها کارآمدتر است. با این حال، Event Sourcing به پیچیدگی میافزاید.
مزایای CQRS عبارتند از:
استفاده از CQRS را در بخش های محدودی از سیستم خود در نظر بگیرید که در آن بیشترین ارزش را دارد.
ویژوال استودیو را باز کنید و یک ASP.NET Core WebApi ایجاد کنید.
بستههای زیر را از طریق Package Manager Console روی پروژه API خود نصب کنید. فقط خطوط زیر را در Package Manager Console خود کپی کنید. تمام بسته های مورد نیاز نصب می شوند.
Install-Package Microsoft.EntityFrameworkCore Install-Package Microsoft.EntityFrameworkCore.Relational Install-Package Microsoft.EntityFrameworkCore.SqlServer Install-Package MediatR Install-Package MediatR.Extensions.Microsoft.DependencyInjection Install-Package Swashbuckle.AspNetCore Install-Package Swashbuckle.AspNetCore.Swagger Install-Package Microsoft.EntityFrameworkCore.Tools
اضافه کردن Model
از آنجایی که ما از یک رویکرد code first پیروی می کنیم، بیایید مدل های داده خود را طراحی کنیم. یک پوشه Models اضافه کنید و یک کلاس جدید به نام Person با propertyهای زیر ایجاد کنید.
public class Person { public Guid Id { get; set; } public string Name { get; set; } public string Family { get; set; } public string NationalCode { get; set; } public string MobileNumber { get; set; } public string Email { get; set; } public string Password { get; set; } }
لطفا همینجا صبر کنید ....
گفتیم ایده CQRS این است که به برنامه اجازه می دهد با مدل های مختلف کار کند. به طور خلاصه، شما یک مدل دارید که داده های مورد نیاز برای به روز رسانی یک رکورد، مدل دیگری برای درج یک رکورد، و مدلی دیگر برای پرس و جو یک رکورد را دارد. این به شما انعطاف پذیری با سناریوهای مختلف و پیچیده می دهد. با اجرای CQRS لازم نیست فقط به یک DTO برای کل عملیات CRUD تکیه کنید.
بنابراین هر عملیات مدل داده خود را خواهد داشت و استفاده از یک مدل مشترک برای تمام عملیات CRUD برخلاف ایده CQRS است در نتیجه در ادامه ما برای هر عملیات مدل دیگری را نیز ایجاد خواهیم کرد.
اضافه کردن Context Class و Interface
یک پوشه جدید به نام Context بسازید و یک کلاس به نام ApplicationContext اضافه کنید. این کلاس خاص به ما کمک می کند تا با استفاده از Entity Framework Core ORM به داده ها دسترسی پیدا کنیم.
public class ApplicationContext : DbContext { public DbSet<Person> Persons { get; set; } public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options) { } public async Task<int> SaveChangesAsync() { return await base.SaveChangesAsync(); } }
نکته - چگونه یک interface را از یک کلاس استخراج کنیم؟
اکنون که کلاس را کامل کردیم، اجازه دهید یک راه آسان برای ایجاد یک interface برای هر کلاس مشخص به شما نشان دهم. ویژوال استودیو بسیار قدرتمند از آن چیزی است که ما فکر می کنیم. بنابراین در اینجا نحوه کار به این صورت است.
خروجی آن به این صورت خواهد بود :
public interface IApplicationContext { DbSet<Person> Persons { get; set; } Task<int> SaveChangesAsync(); }
پیکربندی سرویسهای API برای پشتیبانی از Entity Framework Core
به کلاس Program.cs پروژه API خود بروید. بیایید پشتیبانی از EntityFrameworkCore را اضافه کنیم. فقط این خطوط را اضافه کنید. با این کار EF Core در برنامه Register می شود.
builder.Services.AddDbContext<ApplicationContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly(typeof(ApplicationContext).Assembly.FullName)); });
خط 3 در مورد رشته اتصال که به عنوان DefaultConnection نامگذاری شده است می گوید. اما ما چنین ارتباطی را تعریف نکرده ایم، بیایید آن را نیز انجام دهیم
تعریف رشته اتصال در appsettings.json
ما باید یک منبع داده را به API متصل کنیم. برای این کار، باید یک رشته اتصال در appsettings.json موجود در پروژه API تعریف کنیم.
"ConnectionStrings": { "DefaultConnection": "Server=.;Database=SampleCQRSwithMediatR;Trusted_Connection=True;MultipleActiveResultSets=true" },
ایجاد پایگاه داده
اکنون مدل های خود و رشته اتصال را آماده کرده ایم، تنها کاری که باید انجام دهیم این است که از مدل های تعریف شده یک پایگاه داده تولید کنیم. برای این کار باید از کنسول Package Manager ویژوال استودیو استفاده کنیم. میتوانید این را با رفتن به
Tools -> Nuget Package Manager -> Package Manager Console
قبل از ادامه، بیایید بررسی کنیم که آیا مشکلاتی در Build وجود دارد یا خیر. برنامه را یک بار Build کنید تا مطمئن شوید خطایی وجود ندارد، زیرا ممکن است در مراحل بعدی اخطار مناسبی نشان داده نشود.
و بعد :
add-migration initial
update-database
اکنون، ما این پایگاه داده را به API خود متصل می کنیم تا عملیات CRUD را انجام دهیم.
الگوی Mediator
هنگام ساخت برنامهها، بهویژه برنامههای ASP.NET Core، بسیار مهم است که به خاطر داشته باشید که همیشه باید کدهای داخل controllerها را تا حد امکان به حداقل برسانیم. از نظر تئوری، controllerها فقط مکانیزمهای مسیریابی هستند که یک درخواست را دریافت میکنند و آن را به صورت داخلی به سایر سرویسها ارسال میکنند و دادهها را برمیگردانند. واقعاً منطقی نیست که تمام اعتبارسنجی ها و منطق های خود را در کنترلرها قرار دهید.
الگوی Mediator به مدیریت وابستگی بین اشیا با جلوگیری از ارتباط مستقیم آنها با یکدیگر کمک می کند. در عوض، ارتباط از طریق یک میانجی انجام می شود. به عنوان مثال، یک سرویس درخواست خود را برای میانجی ارسال می کند که به نوبه خود آن را برای پردازش به request handler مربوطه ارسال می کند. با داشتن یک میانجی در این بین، میتوانیم درخواستها را از handler آنها جدا کنیم. لازم نیست فرستنده چیزی در مورد handler بداند.
الگوی Mediator یکی دیگر از الگوهای طراحی است که به طور چشمگیری coupling بین اجزای مختلف یک برنامه کاربردی را با برقراری ارتباط غیرمستقیم، معمولاً از طریق یک special mediator object، کاهش میدهد. اساساً، الگوی Mediator برای پیاده سازی CQRS مناسب است.
کتابخانه MediatR
کتابخانه MediatR به پیاده سازی Mediator Pattern در دات نت کمک می کند. اولین کاری که باید انجام دهیم این است که بسته های آن را نصب کنیم.
پیکربندی MediatR
builder.Services.AddMediatR(Assembly.GetExecutingAssembly());
برخی توسعه دهندگان از دستور فوق که نیازمند استفاده از Reflection در پروژه است استفاده می کنند اما پیشنهاد می کنم بجای آن
یک کلاس خالی با نام فرضی "SampleCQRSwithMediatREntrypointEntrypoint" در پروژه SampleCQRSwithMediatREntrypoint ایجاد کنید، این فقط برای اشاره به اسمبلی پروژه ما ایجاد شده است. می توانید در صورت تمایل به جای آن نام هر یک از Handler ها را اضافه کنید.
services.AddMediatR(typeof(ProjectNameEntrypoint).Assembly);
ایجاد Person Controller
در پوشه Controllers، یک Empty API Controller جدید اضافه کنید و نام آن را PersonController بگذارید.
پیاده سازی عملیات CRUD
عملیات CRUD در اصل مخفف Create، Read، Update و Delete است. اینها اجزای اصلی API های RESTFul هستند. بیایید ببینیم چگونه می توانیم آنها را با استفاده از رویکرد CQRS خود پیاده سازی کنیم.
یک پوشه به نام PersonFeatures در root directory پروژه وداخل آن زیر پوشههای Queries و Command را ایجاد کنید.
پوشه Queries
اینجا جایی است که ما پرس و جوها را اصلاحا wire up می کنیم.
به عنوان مثال GetAllPersons و GetPersonById.
در پوشه PersonFeatures/Queries دو گوشه دیگر به نام های FindPersonById و GetPersonList ایجاد کنید و بترتیب دو کلاس داخل هر یک با نام های GetAllPersonsQuery و GetPersonByIdQuery ایجاد کنید.
کوئری برای دریافت همه محصولات/GetAllProductsQuery
یادآوری مجدد :
گفتیم ایده CQRS این است که به برنامه اجازه می دهد با مدل های مختلف کار کند. به طور خلاصه، شما یک مدل دارید که داده های مورد نیاز برای به روز رسانی یک رکورد، مدل دیگری برای درج یک رکورد، و مدلی دیگر برای پرس و جو یک رکورد را دارد. این به شما انعطاف پذیری با سناریوهای مختلف و پیچیده می دهد. با اجرای CQRS لازم نیست فقط به یک DTO برای کل عملیات CRUD تکیه کنید.
بنابراین می بایست یک مدل(class) برای GetAllPersonQuery در پوشه PersonFeatures/Queries/GetPersonsList ایجاد کنیم
public class GetAllPersonQueryModel : IRequest<List<Person>> { public string Name { get; set; } public string Family { get; set; } public string NationalCode { get; set; } public string MobileNumber { get; set; } public string Email { get; set; } }
و برای کلاس GetAllPersonsQueryHandler :
برای هر درخواست (Query/Command) باید یک handler وجود داشته باشد. Handler تعریف می کند که وقتی مشتری درخواست خاصی را ارسال می کند چه کاری انجام شود.
public class GetAllPersonsQueryHandler : IRequestHandler<GetAllPersonQueryModel, IEnumerable<Person>> { private readonly IApplicationContext _context; public GetAllPersonsQueryHandler(IApplicationContext context) { _context = context; } public async Task<IEnumerable<Person>> Handle(GetAllPersonQueryModel request, CancellationToken cancellationToken) { var personList= await _context.Persons.ToListAsync(); if (personList== null) { return null; } return personList.AsReadOnly(); } }
کوئری برای دریافت محصول بر اساس شناسه/GetProductById
کلاس مدل ما به این صورت خواهد بود:
public class GetPersonByIdQueryModel:IRequest<Person> { public Guid Id { get; set; } }
و برای کلاس GetPersonByIdQueryHandler :
public class GetPersonByIdQueryHandler : IRequestHandler<GetPersonByIdQueryModel, Person> { private readonly IApplicationContext _context; public GetPersonByIdQueryHandler(IApplicationContext context) { _context = context; } public async Task<Person> Handle(GetPersonByIdQueryModel request, CancellationToken cancellationToken) { var person = _context.Persons.Where(a => a.Id == request.Id).FirstOrDefault(); if (person == null) return null; return person; } }
در مسیر ProductFeatures/Commands سه پوشه جدید به نام های Add , Edit , Delete اضافه کنید
و به تناسب نام کلاس های زیر را به پوشه های مربوطه اضافه کنید.
1.CreatePersonCommand
2.DeletePersonByIdCommand
3.UpdatePersonCommand
خوب Command برای Create a New Person :
کلاس مدل ما به این صورت خواهد بود :
public class AddPersonCommandModel:IRequest<Guid> { public string Name { get; set; } public string Family { get; set; } public string NationalCode { get; set; } public string MobileNumber { get; set; } public string Email { get; set; } public string Password { get; set; } public string RepeatPassword { get; set; } }
و برای کلاس CreatePersonCommandHandler :
public class AddPersonCommandHandler : IRequestHandler<AddPersonCommandModel, Guid> { private readonly IApplicationContext _context; public AddPersonCommandHandler(IApplicationContext context) { _context = context; } public async Task<Guid> Handle(AddPersonCommandModel request, CancellationToken cancellationToken) { var person = new Person { Id = Guid.NewGuid(), Name = request.Name, Email = request.Email, Family = request.Family, MobileNumber = request.MobileNumber, NationalCode = request.NationalCode, Password = request.Password }; context.Persons.Add(person); await _context.SaveChangesAsync(); return person.Id; } }
و Command برای Delete a Person By Id :
کلاس مدل ما به این صورت خواهد بود :
public class DeletePersonCommandModel : IRequest<Guid> { public Guid Id { get; set; } }
و برای کلاس DeletePersonCommandHandler :
public class DeletePersonCommandHandler : IRequestHandler<DeletePersonCommandModel, Guid> { private readonly IApplicationContext _context; public DeletePersonCommandHandler(IApplicationContext context) { _context = context; } public async Task<Guid> Handle(DeletePersonCommandModel request, CancellationToken cancellationToken) { var person = await _context.Persons.Where(c => c.Id == request.Id).FirstOrDefaultAsync(); if (person == null) return default; _context.Persons.Remove(person); await _context.SaveChangesAsync(); return person.Id; } }
حالا Command برای Update a Person :
توجه داشته باشید در برنامه های واقعی و بعنوان مثال در اینجا Update معمولا ما تمام اطلاعات مدل خود را به یکباره بروزرسانی نمی کنیم شاید یک مدل جداگانه برای بروزرسانی کلمه عبور داشته باشیم یا یک مدل برای بروزرسانی تصویر پروفایل و ...
کلاس مدل ما به این صورت خواهد بود :
public class EditPersonCommandModel : IRequest<Guid> { public Guid Id { get; set; } public string Name { get; set; } public string Family { get; set; } public string NationalCode { get; set; } public string MobileNumber { get; set; } public string Email { get; set; } }
و برای کلاس EditPersonCommandHandler :
public class EditPersonCommandHandler : IRequestHandler<EditPersonCommandModel, Guid> { private readonly IApplicationContext _context; public EditPersonCommandHandler(IApplicationContext context) { _context = context; } public async Task<Guid> Handle(EditPersonCommandModel request, CancellationToken cancellationToken) { var person = _context.Persons.Where(a => a.Id == request.Id).FirstOrDefault(); if (person == null) { return default; } else { person.Name = request.Name; person.Family = request.Family; person.NationalCode = request.NationalCode; person.MobileNumber = request.MobileNumber; person.Email = request.Email; _context.Persons.Update(person); await _context.SaveChangesAsync(); return person.Id; } } }
بیایید بریم سراغ Person Controller :
[Route("api/[controller]")] [ApiController] public class PersonController : ControllerBase { private readonly IMediator mediator; public PersonController(IMediator mediator) { this.mediator = mediator; } [HttpPost] public async Task<IActionResult> Create(AddPersonCommandModel command) { return Ok(await mediator.Send(command)); } [HttpGet] public async Task<IActionResult> GetAll() { return Ok(await mediator.Send(new GetAllPersonQueryModel())); } [HttpGet("{id}")] public async Task<IActionResult> GetById(Guid id) { return Ok(await mediator.Send(new GetPersonByIdQueryModel { Id = id })); } [HttpDelete("{id}")] public async Task<IActionResult> Delete(Guid id) { return Ok(await mediator.Send(new DeletePersonCommandModel { Id = id })); } [HttpPut("{id}")] public async Task<IActionResult> Update(Guid id, EditPersonCommandModel command) { if (id != command.Id) { return BadRequest(); } return Ok(await mediator.Send(command)); } }
از آنجایی که کار ما تمام شده است، بیایید نتیجه را بررسی کنیم. برای این کار از Swagger UI استفاده می کنیم. ما قبلا package را نصب کرده ایم. بیایید آن را پیکربندی کنیم.
پیکربندی Swagger
این قطعه کد را به Program.cs اضافه کنید:
#region Swagger builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "Sample CQRS With MediatR.WebApi", }); }); #endregion
سپس این Configure method را اضافه کنید:
#region Swagger app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "SampleCQRSwithMediatR.WebApi"); }); #endregion
بیشتر بخوانید : آیا بهمراه CQRS باید از الگوی Repository استفاده کنیم؟
بیشتر بخوانید : Implementing DDD - Clean Architecture
بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core