عنوان:

‫مدیریت خطاها در MediatR با استفاده از Pipeline Behaviors و IRequestExceptionHandler


نویسنده: محمد شریفی
تاریخ: ۱۴۰۳/۰۹/۲۰ ۰۹:۵۵
آدرس: www.dntips.ir
در توسعه نرم‌افزارهای مدرن، مدیریت خطاها از مهم‌ترین چالش‌ها محسوب می‌شود. اگر از MediatR برای مدیریت درخواست‌ها و رویدادها در ASP.NET Core استفاده می‌کنید، احتمالاً با نیاز به مکانیزم‌هایی برای مدیریت خطاهای عمومی و خاص مواجه شده‌اید. این مقاله به بررسی دو ابزار قدرتمند MediatR یعنی Pipeline Behaviors و IRequestExceptionHandler می‌پردازد.

1- Pipeline Behaviors: مدیریت خطاهای عمومی
Pipeline Behaviors در MediatR، امکان مدیریت یکپارچه عملیات‌های مشترک مانند اعتبارسنجی، لاگینگ و مدیریت خطاها را فراهم می‌کند. این مکانیزم به شما اجازه می‌دهد کدی متمرکز برای پردازش درخواست‌ها بنویسید و از تکرار جلوگیری کنید.

مثال:
فرض کنید یک هندلر داریم که درخواست موردنظر را پردازش می‌کند:
public class MyRequestHandler : IRequestHandler<MyRequest, string>
{
    public Task<string> Handle(MyRequest request, CancellationToken cancellationToken)
    {
        if (string.IsNullOrEmpty(request.Input))
            return Task.FromResult("Input cannot be empty");

        throw new InvalidOperationException("An unexpected error occurred during processing.");

        return Task.FromResult($"Processed: {request.Input}");
    }
}

حالا با استفاده از یک Pipeline Behavior، می‌توانیم همه خطاها را در یک نقطه مدیریت کنیم:
public class ExceptionHandlingBehavior<TRequest, TResponse>
    : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<ExceptionHandlingBehavior<TRequest, TResponse>> _logger;

    public ExceptionHandlingBehavior(ILogger<ExceptionHandlingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        try
        {
            return await next();
        }
        catch (Exception ex)
        {
            // Log the exception details
            _logger.LogError(ex, "An error occurred while handling request {Request}", typeof(TRequest).Name);

            // Return a custom response indicating an error occurred
            return CreateErrorResponse<TResponse>("An unexpected error occurred. Please try again later.");
        }
    }

    private TResponse CreateErrorResponse<TResponse>(string errorMessage)
    {
        // You can customize this method to return a specific type of error response
        // For example, a DTO object with error details
        var errorResponse = new
        {
            Success = false,
            ErrorMessage = errorMessage
        };

        // Assuming TResponse can be implicitly cast or converted to the type of errorResponse
        // Replace the return statement below with the appropriate casting logic
        return (TResponse)Convert.ChangeType(errorResponse, typeof(TResponse));
    }
}

برای فعال‌سازی این Behavior، کافی است آن را به Dependency Injection اضافه کنید:
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ExceptionHandlingBehavior<,>));

2- IRequestExceptionHandler: مدیریت خطاهای خاص
گاهی اوقات نیاز داریم که خطاهای یک درخواست خاص را با منطق متفاوت مدیریت کنیم. در این حالت، MediatR اینترفیس IRequestExceptionHandler را ارائه می‌دهد.

مثال:
فرض کنید یک درخواست داریم که باید بررسی کند عدد ورودی مثبت است یا خیر:
public class SpecificRequest : IRequest<string>
{
    public int Number { get; set; }
}

public class SpecificRequestHandler : IRequestHandler<SpecificRequest, string>
{
    public Task<string> Handle(SpecificRequest request, CancellationToken cancellationToken)
    {
        if (request.Number < 0)
            throw new ArgumentOutOfRangeException("Number cannot be negative!");

        return Task.FromResult($"Number is {request.Number}");
    }
}

برای مدیریت خطای خاص این درخواست، یک Exception Handler می‌نویسیم:

public class SpecificRequestExceptionHandler : IRequestExceptionHandler<SpecificRequest, string>
{
    public Task Handle(SpecificRequest request, Exception exception, RequestExceptionHandlerState<string> state, CancellationToken cancellationToken)
    {
        if (exception is ArgumentOutOfRangeException)
        {
            state.SetHandled("The provided number is invalid.");
        }
        else
        {
            state.SetHandled("An unexpected error occurred.");
        }

        return Task.CompletedTask;
    }
}

برای فعال‌سازی این هندلر، آن را به DI اضافه کنید:
services.AddTransient(typeof(IRequestExceptionHandler<,>), typeof(SpecificRequestExceptionHandler));

به طور خلاصه، Pipeline Behaviors به طور کلی برای مدیریت فرآیندهای درخواست‌ها در یک سطح بالاتر و وظایف عمومی مثل لاگینگ یا اعتبارسنجی مناسب‌تر است و IRequestExceptionHandler برای مدیریت استثنائات به صورت تخصصی در سطح پایین‌تر و ایجاد پاسخ‌های مناسب کاربرد دارد.

برای درک بهتر نحوه اجرا و اولویت‌بندی ترکیب Pipeline Behaviors و IRequestExceptionHandler و توضیحات واضح‌تر، بیایید یک مثال واقعی و جزئیات بیشتری را بررسی کنیم.

مثال: فرض کنید شما یک سیستم مدیریت درخواست‌های کاربر دارید که نیازمند مدیریت خاصی از استثنائات و دستورات مختلف است. برای این منظور، از Pipeline Behaviors و IRequestExceptionHandler استفاده می‌کنیم تا فرآیند پردازش درخواست‌ها را بهینه کنیم و مدیریت استثنائات را استاندارد کنیم.

Pipeline Behaviors برای مدیریت خطاهای عمومی
  • وظیفه: در ابتدا، درخواست‌ها از طریق IPipelineBehaviorها پردازش می‌شوند. این رفتارها وظیفه دارند که استثنائات عمومی مانند ArgumentNullException, InvalidOperationException را مدیریت کنند و تصمیم بگیرند که آیا باید پردازش درخواست ادامه یابد یا خیر.

  • مثال: LoggingBehavior یکی از این رفتارها است که لاگ درخواست‌ها و استثنائات را در سطح عمومی مدیریت می‌کند.
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"Handling request: {typeof(TRequest).Name}");

        try
        {
            var response = await next();
            _logger.LogInformation($"Handled request: {typeof(TRequest).Name}");

            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, $"Exception in {typeof(TRequest).Name}: {ex.Message}");
            throw; // Rethrow exception to be handled by IRequestExceptionHandler if not handled here
        }
    }
}
عملکرد:
در ابتدا، LoggingBehavior وارد عمل شده و درخواست‌ها را لاگ می‌کند تا بتواند هرگونه استثنای عمومی مانند ArgumentNullException را مدیریت کند.
اگر خطا رخ دهد، این خطا را لاگ کرده و اجازه می‌دهد تا در صورت نیاز به طور خاص توسط IRequestExceptionHandler مدیریت شود.

IRequestExceptionHandler برای مدیریت استثنائات خاص
  • وظیفه: پس از اینکه Pipeline Behaviors درخواست را پردازش کردند، اگر خطایی رخ دهد که در آن مرحله مدیریت نشده باشد، مسئولیت مدیریت آن به IRequestExceptionHandler واگذار می‌شود. این قسمت از سیستم به صورت تخصصی روی استثنائات خاص مانند ArgumentNullException, InvalidOperationException و سایر خطاهای غیرمنتظره کار می‌کند.

  • مثال: MyRequestExceptionHandler مسئولیت مدیریت استثنائات خاصی را بر عهده دارد و بر اساس نوع استثنا یک پاسخ مناسب ایجاد می‌کند.
public class MyRequestExceptionHandler : IRequestExceptionHandler<MyRequest, string, Exception>
{
    public Task Handle(MyRequest request, Exception exception, RequestExceptionHandlerState<string> state, CancellationToken cancellationToken)
    {
        var response = exception switch
        {
            ArgumentNullException => "Required value is missing.",
            InvalidOperationException => "Invalid operation.",
            _ => "Unexpected error occurred."
        };

        state.SetHandled(response);
        return Task.CompletedTask;
    }
}
عملکرد:
MyRequestExceptionHandler برای مدیریت استثنائات خاصی که ممکن است در Pipeline Behaviorها مدیریت نشده باشند، فراخوانی می‌شود.
این handler بر اساس نوع استثنای رخ داده، یک پیام خطا مناسب تولید می‌کند. به عنوان مثال، برای ArgumentNullException یک پیام خطای مناسب به کاربر باز می‌گرداند که نیاز به یک مقدار از دست رفته است.
اگر این استثنا همچنان مدیریت نشده باقی بماند، درخواست به مرحله بعدی، یعنی Middlewareهای پیاده‌سازی‌شده برای مدیریت استثنائات، واگذار می‌شود.

Middlewareهای پیاده‌سازی‌شده برای مدیریت استثنائات
  • وظیفه: پس از اینکه درخواست از IRequestExceptionHandler عبور کرد، اگر همچنان استثنای رخ داده باشد که قابل مدیریت نبوده است، درخواست به Middlewareهای پیاده‌سازی‌شده برای مدیریت استثنائات فرستاده می‌شود. این Middlewareها معمولاً برای ارسال پاسخ‌های استاندارد به کاربران طراحی شده‌اند.

  • مثال: ExceptionHandlingMiddleware مسئول ارسال پاسخ به کاربر است که توضیح دهد چه اتفاقی افتاده است.
public class ExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlingMiddleware> _logger;

    public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An unexpected error occurred.");
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            await context.Response.WriteAsync("An unexpected error occurred.");
        }
    }
}
عملکرد:
ExceptionHandlingMiddleware وظیفه دارد که پاسخ استانداردی را برای درخواست‌های شکست خورده فراهم کند. برای مثال، اگر یک استثنای غیرمنتظره رخ دهد، Middleware می‌تواند پاسخ HTTP 500 Internal Server Error با یک پیام توضیحی را برگرداند.
این Middleware در آخرین مرحله اجرا می‌شود و مسئولیت ارسال پاسخ‌های استاندارد به کاربر را بر عهده دارد، زمانی که هیچ‌کدام از Pipeline Behaviors و IRequestExceptionHandler قادر به مدیریت استثنای رخ داده نشده باشند.

نحوه اجرا و اولویت‌بندی ترکیب Pipeline Behaviors و IRequestExceptionHandler:
Pipeline Behaviors اجرا می‌شوند:
  • ابتدا درخواست از طریق IPipelineBehaviorها پردازش می‌شود. هر Pipeline Behavior یک وظیفه عمومی را بر عهده دارد، مثلاً لاگ‌کردن درخواست‌ها، اعتبارسنجی ورودی‌ها یا مدیریت استثنائات عمومی.
  • اگر در طول این مرحله خطایی رخ دهد، LoggingBehavior آن را لاگ کرده و تصمیم می‌گیرد که درخواست را ادامه دهد یا متوقف کند.
  • اگر خطایی رخ دهد و LoggingBehavior نتواند آن را مدیریت کند، درخواست به IRequestExceptionHandler فرستاده می‌شود.
IRequestExceptionHandler اجرا می‌شود:
  • پس از اجرای Pipeline Behaviors، اینترفیس IRequestExceptionHandler برای مدیریت استثنائات خاص فراخوانی می‌شود.
  • اگر خطای رخ داده به Pipeline Behaviors مدیریت نشده باشد، MyRequestExceptionHandler نوع خاصی از استثنائات مانند ArgumentNullException یا InvalidOperationException را مدیریت کرده و یک پیام خطا مناسب تولید می‌کند.
  • اگر IRequestExceptionHandler نیز نتواند خطا را مدیریت کند، درخواست به Middlewareهای پیاده‌سازی‌شده برای مدیریت استثناها واگذار می‌شود.
Middlewareهای پیاده‌سازی‌شده برای مدیریت استثناها:
  • اگر IRequestExceptionHandler نیز نتواند خطا را مدیریت کند، درخواست به Middlewareها ارسال می‌شود.
  • این Middlewareها معمولاً به صورت یک لایه اضافی برای ارسال پاسخ‌های استاندارد به کاربر عمل می‌کنند.
  • ExceptionHandlingMiddleware می‌تواند پاسخ‌های HTTP مانند 500 Internal Server Error را برگرداند و جزئیات استثنا را در لاگ‌ها ثبت کند.


جمع‌بندی
در نهایت، ترکیب Pipeline Behaviors و IRequestExceptionHandler و استفاده از Middlewareها به ما کمک می‌کند تا فرآیند مدیریت استثنائات را به‌طور دقیق و موثر در یک سیستم بزرگ پیاده‌سازی کنیم. هر بخش از این سیستم مسئولیت‌های خاص خود را دارد و اولویت‌بندی آنها باعث می‌شود که درخواست‌ها به درستی پردازش شوند و هر نوع استثنای رخ داده به شکل مناسب مدیریت شود.

نظرات

  • محمد شریفی در ۱۴۰۳/۰۹/۲۱ ۱۰:۱۷
    در MediatR علاوه بر <IRequestExceptionHandler<TRequest, TException که برای مدیریت و تغییر نتیجه یک درخواست پس از بروز استثنا استفاده می‌شود، یک اینترفیس دیگری نیز به نام <IRequestExceptionAction<TRequest, TException وجود دارد که برای مدیریت استثناها (exceptions) در زمان اجرای یک درخواست (IRequest) بدون اینکه نتیجه درخواست را تغییر دهیم به کار می‌رود. این قابلیت به شما اجازه می‌دهد که منطق خاصی را هنگام بروز خطا در هنگام پردازش درخواست‌ها پیاده‌سازی کنید.

    ساختار و نحوه کارکرد
    IRequestExceptionAction اینترفیس شامل متدی به شکل زیر است:
    public interface IRequestExceptionAction<TRequest, TException>
        where TRequest : IRequest
        where TException : Exception
    {
        Task Execute(TRequest request, TException exception, CancellationToken cancellationToken);
    }

    پارامترها
    TRequest: نوع درخواستی که استثنا در آن رخ داده است.
    TException: نوع استثنایی که باید مدیریت شود.
    Execute: متدی که باید هنگام بروز استثنا اجرا شود.

    این اینترفیس برای سناریوهایی استفاده می‌شود که نیاز به انجام عملیات خاصی در صورت بروز یک استثنا دارید. برخی از کاربردهای رایج:
    • مدیریت خطاهای حیاتی: ارسال پیام یا SMS به مدیر سیستم هنگام بروز خطای خاصی که بسیار حیاتی است.
    • ثبت گزارشات خطا: ثبت اطلاعات مربوط به خطا و درخواست در سیستم لاگینگ برای تحلیل و رفع مشکلات.
    • اعمال تغییرات بعد از بروز خطا: مثلاً اجرای عملیات بازیابی یا اطلاع‌رسانی به کاربران.
    • منطق شرطی برای خطاهای خاص: مدیریت رفتار در برابر خطاهای مشخص برای یک درخواست خاص.

    مثال پیاده‌سازی
    فرض کنید درخواست خاصی داریم که اگر در پردازش آن یک خطای حیاتی (مثلاً InvalidOperationException) رخ دهد، باید یک پیام فوری به مدیر ارسال شود:
    public class CriticalExceptionAction : IRequestExceptionAction<MyRequest, InvalidOperationException>
    {
        public async Task Execute(MyRequest request, InvalidOperationException exception, CancellationToken cancellationToken)
        {
            // Log the exception
            Console.WriteLine($"Critical exception for request {request.Id}: {exception.Message}");
    
            // Send an SMS or email to the administrator
            await SendCriticalAlertAsync(exception);
    
            // Custom logic can be added here
        }
    
        private Task SendCriticalAlertAsync(Exception exception)
        {
            // Simulate sending an SMS or email
            Console.WriteLine("Sending critical alert to the admin...");
            return Task.CompletedTask;
        }
    }

    در آخر هم برای استفاده از این قابلیت، باید پیاده‌سازی خود را در DI container ثبت کنید:
    services.AddMediatR(cfg =>
    {
        cfg.AddRequestExceptionAction<CriticalExceptionAction>();
    });