عنوان:

‫مدیریت ایونت‌ها در MediatR


نویسنده: محمد شریفی
تاریخ: ۱۴۰۳/۰۹/۱۳ ۱۱:۳۵
آدرس: www.dntips.ir

مقدمه

MediatR یکی از کتابخانه‌های محبوب در اکوسیستم NET. است که به کاهش وابستگی بین اجزای مختلف سیستم کمک می‌کند. یکی از ویژگی‌های کلیدی این کتابخانه، مدیریت ایونت‌ها است که شامل پشتیبانی از چندین هندلر و اجرای آن‌ها به صورت Broadcast می‌باشد.
در این مقاله علاوه بر نحوه کار با ایونت‌ها، به موضوع کنترل ترتیب اجرای هندلرها می‌پردازیم و مثالی کامل از پیاده‌سازی این قابلیت ارائه می‌کنیم.

مفهوم ایونت‌ها در MediatR

در MediatR، ایونت‌ها با استفاده از اینترفیس INotification تعریف می‌شوند. Event به معنای اعلان (Notification) است که می‌تواند توسط یک یا چند هندلر پردازش شود.

ویژگی‌های ایونت در MediatR:

  1. Broadcasting: هر ایونت می‌تواند چندین هندلر داشته باشد.
  2. عدم ترتیب پیش‌فرض: هندلرها به ترتیب خاصی اجرا نمی‌شوند، مگر اینکه ترتیب آن‌ها را مشخص کنید.
  3. غیرهمزمانی: ایونت‌ها می‌توانند به صورت همزمان و غیرهمزمان هندل شوند.


مراحل استفاده از ایونت‌ها در MediatR

۱. نصب وابستگی‌های موردنیاز

ابتدا MediatR و بسته‌های مرتبط را نصب کنید:
dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

۲. تعریف ایونت

یک کلاس که از INotification ارث‌بری می‌کند، به عنوان ایونت تعریف می‌کنیم:
using MediatR;

public class OrderCreatedEvent : INotification
{
    public int OrderId { get; set; }
    public DateTime CreatedAt { get; set; }

    public OrderCreatedEvent(int orderId, DateTime createdAt)
    {
        OrderId = orderId;
        CreatedAt = createdAt;
    }
}

۳. تعریف چند هندلر برای ایونت

چند هندلر برای مدیریت ایونت تعریف می‌کنیم:
using MediatR;

public class LogOrderHandler : INotificationHandler<OrderCreatedEvent>
{
    public Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"[1] Logging Order Created: {notification.OrderId}");
        return Task.CompletedTask;
    }
}

public class SendEmailHandler : INotificationHandler<OrderCreatedEvent>
{
    public Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"[2] Sending Email for Order: {notification.OrderId}");
        return Task.CompletedTask;
    }
}

public class UpdateInventoryHandler : INotificationHandler<OrderCreatedEvent>
{
    public Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"[3] Updating Inventory for Order: {notification.OrderId}");
        return Task.CompletedTask;
    }
}

ترتیب اجرای هندلرها (Custom Execution Order)

به صورت پیش‌فرض، MediatR ترتیب مشخصی برای اجرای هندلرها ندارد. برای کنترل این ترتیب، می‌توانید از الگوی Decorator Pattern استفاده کنید.

۴. پیاده‌سازی Decorator برای ترتیب هندلرها

ابتدا یک Decorator پیاده‌سازی می‌کنیم که هندلرها را بر اساس اولویت اجرا کند:
using MediatR;

public class OrderedNotificationHandler<TNotification> : INotificationHandler<TNotification>
    where TNotification : INotification
{
    private readonly IEnumerable<INotificationHandler<TNotification>> _handlers;

    public OrderedNotificationHandler(IEnumerable<INotificationHandler<TNotification>> handlers)
    {
        _handlers = handlers;
    }

    public async Task Handle(TNotification notification, CancellationToken cancellationToken)
    {
        foreach (var handler in _handlers.OrderBy(h => GetPriority(h)))
        {
            await handler.Handle(notification, cancellationToken);
        }
    }

    private int GetPriority(INotificationHandler<TNotification> handler)
    {
        var priorityAttribute = handler.GetType()
            .GetCustomAttributes(typeof(HandlerPriorityAttribute), true)
            .FirstOrDefault() as HandlerPriorityAttribute;

        return priorityAttribute?.Priority ?? int.MaxValue; 
    }
}

[AttributeUsage(AttributeTargets.Class)]
public class HandlerPriorityAttribute : Attribute
{
    public int Priority { get; }
    public HandlerPriorityAttribute(int priority) => Priority = priority;
}

۵. افزودن اولویت به هندلرها

برای تعیین ترتیب، به هندلرها یک Attribute اضافه می‌کنیم:
[HandlerPriority(1)]
public class LogOrderHandler : INotificationHandler<OrderCreatedEvent>
{
    public Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"[1] Logging Order Created: {notification.OrderId}");
        return Task.CompletedTask;
    }
}

[HandlerPriority(2)]
public class SendEmailHandler : INotificationHandler<OrderCreatedEvent>
{
    public Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"[2] Sending Email for Order: {notification.OrderId}");
        return Task.CompletedTask;
    }
}

[HandlerPriority(3)]
public class UpdateInventoryHandler : INotificationHandler<OrderCreatedEvent>
{
    public Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"[3] Updating Inventory for Order: {notification.OrderId}");
        return Task.CompletedTask;
    }
}

۶. ثبت Decorator در DI

برای استفاده از Decorator، باید آن را در Dependency Injection رجیستر کنید:
using MediatR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMediatR(typeof(Program));

builder.Services.Decorate(typeof(INotificationHandler<>), typeof(OrderedNotificationHandler<>));

var app = builder.Build();
app.Run();

۷. انتشار ایونت

مانند قبل، یک ایونت منتشر می‌کنیم:
using MediatR;

public class OrderService
{
    private readonly IMediator _mediator;

    public OrderService(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task CreateOrder(int orderId)
    {
        Console.WriteLine("Creating Order...");

        var orderCreatedEvent = new OrderCreatedEvent(orderId, DateTime.UtcNow);
        await _mediator.Publish(orderCreatedEvent);
    }
}
با اجرای برنامه، هندلرها بر اساس ترتیب تعیین‌شده (اولویت) اجرا می‌شوند:
Creating Order...
[1] Logging Order Created: 123
[2] Sending Email for Order: 123
[3] Updating Inventory for Order: 123

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

نتیجه‌گیری
MediatR ابزاری قدرتمند برای مدیریت ایونت‌ها و ساده‌سازی معماری سیستم است. با استفاده از Decorator Pattern، می‌توانید کنترل بیشتری روی ترتیب اجرای هندلرها داشته باشید. این قابلیت به خصوص در سیستم‌های پیچیده که نیاز به هماهنگی دقیق میان عملیات دارند، بسیار مفید است.

نظرات

  • محمد شریفی در ۱۴۰۳/۰۹/۱۴ ۱۶:۴۴
    نکته تکمیلی:
    عملیات‌هایی که روی دیتابیس اثر می‌گذارند و ممکن است به خطا برخورد کنند (مانند ذخیره‌سازی، بروزرسانی یا درج داده‌ها)، بهتر است به جای اجرای مستقیم در NotificationHandler، به یک بک‌گراند سرویس (مانند Hangfire) منتقل شوند. این کار چندین مزیت کلیدی دارد:
    1. قابلیت Retry: اگر عملیات با خطا مواجه شد (به دلیل مشکلاتی مانند قطعی دیتابیس یا محدودیت‌های تراکنش)، بک‌گراند سرویس به طور خودکار یا بر اساس تنظیمات، عملیات را مجدداً اجرا می‌کند.
    2. پایداری بیشتر:NotificationHandlerها معمولاً برای مدیریت رویدادهای کوتاه‌مدت طراحی شده‌اند. انتقال عملیات طولانی یا پیچیده به بک‌گراند سرویس از قفل شدن منابع و افزایش زمان اجرای Event جلوگیری می‌کند.
    3. ردیابی و نظارت بر وظایف: سرویس‌هایی مانند Hangfire امکان ردیابی وضعیت وظایف (Jobs) را فراهم می‌کنند. این موضوع به شما کمک می‌کند وظایف شکست‌خورده را شناسایی کرده و به صورت دستی یا خودکار آنها را دوباره اجرا کنید.
    4. مقیاس‌پذیری بهتر: عملیات‌های سنگین و وابسته به منابع، به جای اینکه بار زیادی روی سیستم اصلی ایجاد کنند، در سرویس‌های جداگانه اجرا می‌شوند که امکان توزیع بار (Load Balancing) را فراهم می‌کند.

  • یاسین اسدنژاد در ۱۴۰۳/۰۹/۱۴ ۱۷:۲۹
    عالی. فقط اگه بخواهیم بجای CreateOrder یه هندلر بزاریم اونوقت چی؟ CreateOrderHandler .

    • محمد شریفی در ۱۴۰۳/۰۹/۱۶ ۱۱:۳۱

      CreateOrder یک متد از OrderService است که وظیفه ایجاد سفارش را بر عهده دارد. کد مربوط به ایجاد سفارش را می‌توان به یک RequestHandler (مانند CreateOrderHandler) منتقل کرد. با این حال، بهتر است ایونت‌هایی مانند OrderCreatedEvent خارج از RequestHandler مدیریت شوند.

      دلیل این پیشنهاد این است که RequestHandler باید تنها مسئولیت اجرای درخواست مربوطه را بر عهده داشته باشد. عملیات مرتبط با ایونت‌ها بهتر است به‌صورت مستقیم در سرویس فراخوانی شوند تا ایونت‌ها از Handler مجزا باقی بمانند.

      این رویکرد باعث حفظ جداسازی مسئولیت‌ها و افزایش خوانایی کد می‌شود.


  • محمد شریفی در ۱۴۰۳/۰۹/۱۷ ۱۱:۱۲

    نکته‌ی تکمیلی

    دو نوع استراتژی برای انتشار ایونت ها در MediatR وجود دارد قبل از نسخه ورژن 12، هر هندلر به طور جداگانه فراخوانی می شود اما اکنون، یک اینترفیس جدید INotificationPublisherوجود دارد که نحوه فراخوانی هندلرها را کنترل می‌کند.

    پیاده‌سازی پیش‌فرض این اینترفیس، ForeachAwaitPublisher است:
    public class ForeachAwaitPublisher : INotificationPublisher
    {
        public async Task Publish(
            IEnumerable<NotificationHandlerExecutor> handlerExecutors,
            INotification notification,
            CancellationToken cancellationToken)
        {
            foreach (var handler in handlerExecutors)
            {
                await handler
                    .HandlerCallback(notification, cancellationToken)
                    .ConfigureAwait(false);
            }
        }
    }
    اما اکنون می‌توان از TaskWhenAllPublisher نیز استفاده کرد:
    public class TaskWhenAllPublisher : INotificationPublisher
    {
        public Task Publish(
            IEnumerable<NotificationHandlerExecutor> handlerExecutors,
            INotification notification,
            CancellationToken cancellationToken)
        {
            var tasks = handlerExecutors
                .Select(handler => handler.HandlerCallback(
                    notification,
                    cancellationToken))
                .ToArray();
    
            return Task.WhenAll(tasks);
        }
    }
    مقایسه این دو استراتژی:
    • ForeachAwaitPublisher:
      • هر هندلر را به ترتیب فراخوانی می‌کند.
      • در صورت وقوع یک استثناء در یکی از هندلرها، اجرای تمامی هندلرها متوقف می‌شود.
    • TaskWhenAllPublisher:
      • همه هندلرها را به طور همزمان فراخوانی می‌کند.
      • اجرای تمامی هندلرها را صرف نظر از اینکه یکی از آنها استثناء رخ دهد، ادامه می‌دهد.
      • اگر task برگشتی از TaskWhenAllPublisher را ذخیره کنید، می‌توانید به Property Task.Exception دسترسی داشته باشید که شامل یک AggregateException است و می‌توانید پیاده‌سازی برخورد بهتر با استثناء‌ها را پیاده‌سازی کنید.

    برای پیکربندی نحوه استفاده MediatR از استراتژی‌های INotificationPublisher، باید یک Action از نوع MediatRServiceConfiguration را ارائه دهید و MediatRServiceConfiguration را پیکربندی کنید.
    اگر می‌خواهید از استراتژی TaskWhenAllPublisher استفاده کنید، می‌توانید:
    • مقداری برای NotificationPublisher اختصاص دهید.
    • نوع استراتژی را در NotificationPublisherType مشخص کنید:
    services.AddMediatR(config => {
        config.RegisterServicesFromAssemblyContaining<Program>();
    
        // Setting the publisher directly will make the instance a Singleton.
        config.NotificationPublisher = new TaskWhenAllPublisher();
    
        // Setting the publisher type will:
        // 1. Override the value set on NotificationPublisher
        // 2. Use the service lifetime from the ServiceLifetime property below
        config.NotificationPublisherType = typeof(TaskWhenAllPublisher);
    
        config.ServiceLifetime = ServiceLifetime.Transient;
    });