endpoints.MapGet("/api/authors", async (MinimalBlogDbContext ctx) => { var authors = await ctx.Authors.ToListAsync(); return authors; }); endpoints.MapPost("/api/authors", async (MinimalBlogDbContext ctx, AuthorDto authorDto) => { var author = new Author(); author.FirstName = authorDto.FirstName; author.LastName = authorDto.LastName; author.Bio = authorDto.Bio; author.DateOfBirth = authorDto.DateOfBirth; ctx.Authors.Add(author); await ctx.SaveChangesAsync(); return author; });
ایجاد Command و هندلر مخصوص ایجاد یک نویسندهی جدید
در الگوی CQRS، یک دستور، کاری را بر روی بانک اطلاعاتی انجام میدهد. برای مثال در اینجا قرار است نویسندهای را ثبت کند. در ادامه میخواهیم بدنهی endpoints.MapPost فوق را با الگوی CQRS انطباق دهیم. به همین جهت به یک Command نیاز داریم:
using MediatR; using MinimalBlog.Domain.Model; namespace MinimalBlog.Api.Features.Authors; public class CreateAuthorCommand : IRequest<Author> { public AuthorDto AuthorDto { get; set; } = default!; }
public interface IRequest<out TResponse> : IBaseRequest
سپس نیاز به یک هندلر است تا دستور رسیده را پردازش کند:
namespace MinimalBlog.Api.Features.Authors; public class CreateAuthorCommandHandler : IRequestHandler<CreateAuthorCommand, Author> { private readonly MinimalBlogDbContext _context; private readonly IMapper _mapper; public CreateAuthorCommandHandler(MinimalBlogDbContext context, IMapper mapper) { _context = context ?? throw new ArgumentNullException(nameof(context)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); } public async Task<Author> Handle(CreateAuthorCommand request, CancellationToken cancellationToken) { if (request == null) { throw new ArgumentNullException(nameof(request)); } var toAdd = _mapper.Map<Author>(request.AuthorDto); _context.Authors.Add(toAdd); await _context.SaveChangesAsync(cancellationToken); return toAdd; } }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse>
پس از این تغییرات، بدنهی lambda expression مربوط به endpoints.MapPost به صورت زیر تغییر کرده و ساده میشود:
endpoints.MapPost("/api/authors", async (IMediator mediator, AuthorDto authorDto) => { var command = new CreateAuthorCommand { AuthorDto = authorDto }; var author = await mediator.Send(command); return author; });
نکته 1: await داخل بدنهی lambda expression مربوط به endpoints را فراموش نکنید. تمام متدهای IMediator از نوع aysnc هستند؛ هرچند روش نامگذاری SendAsync را رعایت نکردهاند و اگر این await فراموش شود، مشاهده خواهید کرد که برنامه در حین فراخوانی endpoints در مرورگر، در حالت هنگ و صبر کردن نامحدود قرار میگیرد، بدون اینکه کاری را انجام دهد و یا حتی استثنایی را صادر کند.
نکته 2: در پیاده سازی هندلر، استفاده از cancellationToken را نیز مشاهده میکنید. تقریبا تمام متدهای async مربوط به EF-Core به همراه پارامتری جهت دریافت cancellationToken هم هستند. اگر کاربری قصد لغو یک درخواست طولانی را داشته باشد و بر روی دکمهی stop مرورگر کلیک کند و یا حتی صفحه را چندین بار ریفرش کند، این به معنای abort درخواست(های) رسیدهاست. وجود این cancellationTokenها، بار سرور را کاهش داده و عملیات در حال اجرای سمت سرور را در یک چنین حالتهایی متوقف میکند.
البته هندلری که در اینجا تعریف شده، این cancellationToken را باید از mediator دریافت کند که در کدهای endpoint فوق، چنین نیست. برای رفع این مشکل باید به صورت زیر عمل کرد:
endpoints.MapGet("/api/authors", async (IMediator mediator, CancellationToken ct) => { var request = new GetAllAuthorsQuery(); var authors = await mediator.Send(request, ct); return authors; });
نکته 3: هندلرها عموما چیزی را بازگشت نمیدهند؛ صرف نظر از هندلر فوق که نیاز بوده تا Id شیء ذخیره شده را بازگشت دهد، عموما به همراه هیچ خروجی نیستند. به همین جهت در حین تعریف آنها فقط کافی است در آرگومانهای جنریک آنها، نوع خروجی را ذکر نکنیم:
public class Handler : IRequestHandler<Command>
public interface IRequestHandler<in TRequest> : IRequestHandler<TRequest, Unit> where TRequest : IRequest<Unit>
public async Task<Unit> Handle(Command request, CancellationToken cancellationToken) { // ... return Unit.Value; }
public class Command : IRequest
ایجاد Query و هندلر مخصوص بازگشت لیست نویسندهها
در الگوی CQRS، یک کوئری قرار است اطلاعاتی را بازگشت دهد و ... وضعیت بانک اطلاعاتی را تغییر نمیدهد. بنابراین در اینجا یک IRequest که قرار است لیستی از نویسندگان را بازگشت دهد، تعریف میکنیم. بدنهی آن هم میتواند خالی باشد و یا به همراه خواصی مانند اطلاعات صفحه بندی و یا مرتب سازی گزارشگیری رسیدهی از درخواست:
using MediatR; using MinimalBlog.Domain.Model; namespace MinimalBlog.Api.Features.Authors; public class GetAllAuthorsQuery : IRequest<List<Author>> { }
namespace MinimalBlog.Api.Features.Authors; public class GetAllAuthorsHandler : IRequestHandler<GetAllAuthorsQuery, List<Author>> { private readonly MinimalBlogDbContext _context; public GetAllAuthorsHandler(MinimalBlogDbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } public Task<List<Author>> Handle(GetAllAuthorsQuery request, CancellationToken cancellationToken) { return _context.Authors.ToListAsync(cancellationToken); } }
endpoints.MapGet("/api/authors", async (IMediator mediator) => { var request = new GetAllAuthorsQuery(); var authors = await mediator.Send(request); return authors; });
و یا حتی معماری CQRS با معماری Event store نیز قابل ترکیب است:
در اینجا بجای استفاده از بانک اطلاعاتی Write، از یک Event store استفاده میشود. کار event store، دریافت رویدادهای write است و سپس باز پخش آنها به بانک اطلاعاتی Read؛ تا کار همگام سازی به این نحو صورت گیرد.
روشی برای نظم دادن به نحوهی تعریف کلاسهای الگوی CQRS
تا اینجا برای مثال کلاسCreateAuthorCommand را در یک فایل مجزا و سپس هندلر آنرا به نام CreateAuthorCommandHandler در یک فایل دیگر تعریف کردیم. میتوان جهت بالابردن خوانایی برنامه، کاهش رفت و برگشتها برای یافتن کلاسهای مرتبط و همچنین سهولت یافتن هندلرهای مرتبط با هر متد mediator.Send، از روش زیر نیز استفاده کرد:
public static class CreateAuthor { public class Command : IRequest<AuthorGetDto> { // ... } public class Handler : IRequestHandler<Command, AuthorGetDto> { // ... } }
var command = new CreateAuthor.Command { AuthorDto = authorDto }; var author = await mediator.Send(command, ct);
در مورد کوئریها هم میتوان به قالب مشابهی رسید که در اینجا هم کوئری و هندلر آن، ذیل نام اصلی مدنظر قرار میگیرند:
public static class GetAllAuthors { public class Query : IRequest<List<AuthorGetDto>> { //... } public class Handler : IRequestHandler<Query, List<AuthorGetDto>> { //... } }