عنوان:

‫بازنگری در استفاده از MediatR در توسعه برنامه‌های ASP.NET Core


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۲/۰۷ ۰۸:۱۲
آدرس: www.dntips.ir
مقدمه
جیمی بوگارد (Jimmy Bogard)، خالق کتابخانه محبوب MediatR، اخیراً اعلام کرده است که این کتابخانه به صورت تجاری عرضه خواهد شد. این تغییر، فرصتی مناسب برای توسعه‌دهندگان برنامه‌های ASP.NET Core فراهم می‌آورد تا نیازمندی‌های خود به این کتابخانه را مورد ارزیابی مجدد قرار دهند و در صورت لزوم، به دنبال راهکارهای جایگزین باشند. سال‌هاست که MediatR به عنوان معماری پیش‌فرض برای برنامه‌های جدی ASP.NET Core در نظر گرفته می‌شود و به عنوان یک بلوک ساختمانی اساسی برای الگوهای CQRS (فرمان جداسازی مسئولیت پرس و جو - Command Query Responsibility Segregation)، جداسازی تمیز (Clean Separation) و قابلیت تست‌پذیری (Testability) شناخته می‌شود. با این حال، بررسی کاربرد آن در پروژه‌های واقعی نشان می‌دهد که مزایای آن بیشتر جنبه نظری دارند، در حالی که هزینه‌های مربوط به بویلرپلیت (Boilerplate)، غیرمستقیم‌سازی (Indirection) و زمان توسعه، کاملاً محسوس هستند. خوشبختانه، روش‌های بهتر و ساده‌تری برای ساخت APIها با استفاده از Minimal APIs (حداقل APIها) و معماری برش عمودی (Vertical Slice Architecture) در حال ظهور هستند و به دلایل موجهی محبوبیت پیدا می‌کنند. این رویکرد، برای توسعه‌دهندگانی که به دنبال نوشتن کد واضح، قابل نگهداری و بدون پیچیدگی‌های غیرضروری هستند، ارزش یادگیری دارد.

MediatR چیست و چرا مورد استفاده قرار می‌گیرد؟
MediatR یک کتابخانه متن‌باز (Open-source) پرطرفدار است که الگوی میانجی (Mediator Pattern) را پیاده‌سازی می‌کند. در این الگو، یک درخواست (Command یا Query) به یک میانجی (ISender یا IMediator) ارسال می‌شود که آن را به یک هندلر (Handler) مناسب هدایت می‌کند. هدف از استفاده از MediatR، کمک به موارد زیر است:
  • جداسازی APIها از منطق کسب و کار (Decouple APIs from business logic)
  • بهبود قابلیت تست‌پذیری (Improve testability)
  • فعال‌سازی منطق‌های برش عرضی (Cross-cutting logic) قابل استفاده مجدد از طریق رفتارهای خط لوله (Pipeline Behaviors)

با این حال، یک تصور نادرست در میان بسیاری از توسعه‌دهندگان وجود دارد که MediatR را معادل با CQRS می‌دانند. حقیقت این است که اکثر تیم‌های توسعه‌دهنده، CQRS واقعی را پیاده‌سازی نمی‌کنند و صرفاً خواندن و نوشتن اطلاعات را در کلاس‌های جداگانه انجام می‌دهند که در نهایت به یک پایگاه داده واحد متصل می‌شوند. این رویکرد، CQRS واقعی نیست و تنها یک لایه غیرضروری بدون مزیت واقعی ایجاد می‌کند. برای انجام چنین کاری، نیازی به MediatR نیست و در بسیاری از موارد، نیازی به خود الگوی CQRS نیز وجود ندارد.

پیاده‌سازی یک Endpoint سفارش مبتنی بر MediatR
برای روشن شدن موضوع، فرض کنید می‌خواهیم یک Endpoint نوع POST را پیاده‌سازی کنیم که یک سفارش را با استفاده از EF Core (هسته فریم‌ورک موجودیت - Entity Framework Core) در PostgreSQL ذخیره می‌کند. در دنیای MediatR، پیاده‌سازی Endpoint به این صورت خواهد بود:
[HttpPost("/orders")]
public async Task<IActionResult> CreateOrder(CreateOrderCommand command, IMediator mediator)
{
    var result = await mediator.Send(command);
    return result.Match(
        onSuccess: orderId => CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId),
        onFailure: errors => Problem(errors)
    );
}
سپس، منطق اصلی در یک کلاس هندلر جداگانه به همراه چند نوع پشتیبانی‌کننده دیگر پیاده‌سازی می‌شود:
public record CreateOrderCommand(Guid CustomerId, List<OrderItem> Items) : IRequest<Result<Guid>>;

public record OrderItem(Guid ProductId, int Quantity, decimal Price);

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
    private readonly ApplicationDbContext _dbContext;

    public CreateOrderCommandHandler(ApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public async Task<Result<Guid>> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = new Order
        {
            CustomerId = request.CustomerId,
            Items = request.Items.Select(i => new OrderItem
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                Price = i.Price
            }).ToList()
        };

        _dbContext.Orders.Add(order);
        await _dbContext.SaveChangesAsync(cancellationToken);

        return Result.Success(order.Id);
    }
}
همانطور که مشاهده می‌شود، این رویکرد شامل مراحل و عناصر اضافی متعددی است:
  • یک شیء فرمان (Command object) با نام CreateOrderCommand
  • یک شیء نتیجه (Result object) با نام Result<Guid>
  • یک کلاس هندلر (Handler class) با نام CreateOrderCommandHandler که رابط IRequestHandler را پیاده‌سازی می‌کند.
  • یک مرحله ثبت میانجی (Mediator registration step) برای اتصال فرمان و هندلر.
  • یک Endpoint که وظیفه‌ای جز انتقال درخواست به هندلر ندارد.

تمام این مراحل فقط برای ذخیره یک یا چند ردیف در پایگاه داده انجام می‌شود. در ادامه، روش انجام همین کار بدون استفاده از MediatR بررسی خواهد شد.

استفاده از Minimal APIs و معماری برش عمودی
در مقابل، پیاده‌سازی دقیقاً همین رفتار با استفاده از معماری برش عمودی (Vertical Slice Architecture) و Minimal APIs به صورت زیر و در قالب یک کلاس Endpoint قابل انجام است:
public class CreateOrderEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapPost("/orders", async (CreateOrderRequest request, ApplicationDbContext dbContext) =>
        {
            var order = new Order
            {
                CustomerId = request.CustomerId,
                Items = request.Items.Select(i => new OrderItem
                {
                    ProductId = i.ProductId,
                    Quantity = i.Quantity,
                    Price = i.Price
                }).ToList()
            };

            dbContext.Orders.Add(order);
            await dbContext.SaveChangesAsync();

            return Results.Created($"/orders/{order.Id}", order.Id);
        });
    }
}

public record CreateOrderRequest(Guid CustomerId, List<OrderItemRequest> Items);
public record OrderItemRequest(Guid ProductId, int Quantity, decimal Price);
سپس، تنها کاری که باید انجام شود، نگاشت (Map) این ویژگی/Endpoint در فایل Program.cs به این صورت است:
app.AddEndpoint<CreateOrderEndpoint>();
همانطور که مشاهده می‌شود، در این رویکرد:
  • نیازی به میانجی (Mediator) نیست.
  • نیازی به هندلر (Handler) جداگانه نیست.
  • نیازی به فایل‌های اضافی فقط برای انتقال داده به پایگاه داده نیست.

همه چیز در یک مکان قابل مشاهده است. این امر منجر به تسریع در توسعه و رفع اشکال (Debug) می‌شود و به توسعه‌دهندگان اجازه می‌دهد تا بر روی منطق اصلی تمرکز کرده و از موارد غیرضروری اجتناب کنند.

رفتارهای خط لوله (Pipeline Behaviors) چه می‌شوند؟
طرفداران MediatR اغلب از رابط IPipelineBehavior<TRequest, TResponse> برای اعتبارسنجی (Validation) و ثبت وقایع (Logging) به عنوان یکی از مزایای این کتابخانه یاد می‌کنند. با این حال، برای اعتبارسنجی یک DTO (شیء انتقال داده - Data Transfer Object)، نیازی به یک خط لوله پیچیده نیست و می‌توان این کار را به صورت مستقیم و واضح انجام داد:
public record CreateOrderRequest(Guid CustomerId, [NotEmpty] List<OrderItemRequest> Items);
public record OrderItemRequest(Guid ProductId, [Range(1, 100)] int Quantity, decimal Price);
این رویکرد، تمیز، صریح و قابل تست است و از کلاس‌های رفتار پنهان (Hidden behavior classes)، فیلترهای سراسری جادویی (Magic global filters) و پشته‌های فراخوانی (Stack traces) پیچیده جلوگیری می‌کند.

آیا هندلرهای MediatR تست‌پذیرتر نیستند؟
این ادعا تنها در صورتی صادق است که تمرکز بر تست کردن موارد اشتباه باشد. هندلرهایی که صرفاً داده‌ها را منتقل می‌کنند، منطق تجاری واقعی ندارند و تست واحد (Unit test) برای آن‌ها مناسب نیست. این نوع رفتارها باید از طریق تست‌های یکپارچگی (Integration test) و با استفاده از WebApplicationFactory و بررسی رفتار کامل از طریق لایه HTTP مورد ارزیابی قرار گیرند. اگر منطق تجاری واقعی (مانند محاسبات تخفیف یا تشخیص تقلب) وجود داشته باشد، باید آن را در کلاس‌های جداگانه استخراج کرده و در همانجا تست کرد. برای نوشتن تست‌های خوب، نیازی به انتزاع هندلر (Handler abstraction) نیست، بلکه به مرزهایی نیاز است که واقعاً اهمیت دارند.

نتیجه‌گیری
MediatR هرگز قرار نبوده است که معماری پیش‌فرض باشد و تنها یک ابزار در میان سایر ابزارها است. در اکثر APIهای دات‌نت امروزی، این ابزار، انتخاب مناسبی نیست. با تجاری شدن این کتابخانه، اکنون زمان آن رسیده است که از خود بپرسید آیا واقعاً به آن نیاز دارید؟ با استفاده از معماری برش عمودی و Minimal APIs، مزایای زیر حاصل می‌شود:
  • تعداد فایل‌های کمتر
  • ساختار تمیزتر
  • تجربه توسعه روان‌تر

توصیه می‌شود از قابلیت‌های پلتفرم دات‌نت استفاده کنید، Endpointهای واضح و متمرکز را به عنوان ویژگی‌ها پیاده‌سازی کنید و از انتزاع‌های غیرضروری موارد ساده اجتناب کنید.


مشاهده مطلب اصلی