معالجة الاستثناءات المخصصة على .NET API

· 4 دقيقة قراءة

الاستثناءات سيئة، ونحن نعلم أليس كذلك؟ ولكن ماذا لو كان علينا التعامل معهم؟

ماذا يحدث عندما يكون لدينا استثناء، على سبيل المثال، في واجهة برمجة التطبيقات (API)، فإنه يعرض رسالة مكدسة تتضمن الكثير من المعلومات التي قد نرغب في إزالتها من الاستجابة التي يتلقاها مستخدمونا.

بالنسبة للعرض التوضيحي، قمت بإنشاء واجهة برمجة تطبيقات dotnet وأضفت طريقة من شأنها طرح استثناء:

[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
    throw new Exception("This is a custom exception!");
}

إذا قمنا بطرح استثناء، فسيبدو كما يلي:

System.Exception: This is a custom exception!
   at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
   at lambda_method16(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

يبدو هذا طبيعيًا، وهذا ما تحصل عليه من الاستثناء، ولكن بالنسبة لي، فإنه يعرض الكثير من المعلومات، إذا كان لديك مكتبات وأشياء، فقد يعرض معلومات معقولة تتعلق بعميلك أو مشروعك أو أشياء أخرى لشخص قد يحاول رؤية الأشياء.

وهذا ما يبدو على Swagger:

إنشاء استثناء مخصص

يمكن تجنب هذه الخطوة، لأننا نعرف ما هو الاستثناء الذي سيتم طرحه، في هذه الحالة Exception، ولكن بالنسبة لي، فإن الحصول على استثناءاتك المخصصة هو الأفضل نظرًا لأن لديك المزيد من التحكم في ما ترميه.

في هذه الحالة، قمت للتو بإنشاء كائن CustomException يرث من Exception:

namespace CustomExceptionHandleDemo.Exceptions
{
    public class CustomException : Exception
    {
        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        public CustomException() { }

        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        /// <param cref="string" name="message">Parameter for message</param>
        public CustomException(string message) : base(message) { }

        /// <summary>
        /// Constructor for <see cref="CustomException"/>
        /// </summary>
        /// <param cref="string" name="message">Parameter for message</param>
        /// <param cref="Exception" name="inner">Parameter for inner</param>
        public CustomException(string message, Exception inner) : base(message, inner) { }
    }
}

بعد أن قمنا بإنشاء الاستثناء المخصص، فلنقم بتحديث طريقتنا لرمي CustomException بدلاً من Exception:

[HttpGet]
[Route("GetWithoutExceptionHandler")]
public Task GetWithoutExceptionHandler()
{
    throw new CustomException("This is a custom exception!");
}

لن يغير هذا أي شيء في الوقت الحالي ولكن تتبع المكدس سيُظهر أن الكائن الذي تم طرحه هو CustomException بدلاً من Exception، ألقِ نظرة على بداية تتبع المكدس:

CustomExceptionHandleDemo.Exceptions.CustomException: This is a custom exception!
   at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
   at lambda_method24(Closure , Object , Object[] )
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Logged|12_1(ControllerActionInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeNextActionFilterAsync>g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
   at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

إنشاء ExceptionFilterAttribute

لقد منحتنا Microsoft طريقة للتعامل مع الاستثناءات بعد طرحها، ويمكنك التحقق من مزيد من المعلومات هنا.

لكن الوثائق التي قدموها لنا حتى الآن هي:

عامل تصفية مجردة يتم تشغيله بشكل غير متزامن بعد قيام الإجراء بطرح استثناء. يجب أن تتجاوز الفئات الفرعية OnException(ExceptionContext) أو OnExceptionAsync(ExceptionContext) ولكن ليس كليهما.

لذا فلنقم بإنشاء واحد، سنقوم بإنشاء CustomExceptionFilterAttribute حيث سنقوم بتجاوز OnException:

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using CustomExceptionHandleDemo.Exceptions;

namespace CustomExceptionHandleDemo.ExceptionFilterAttributes
{
    public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
    {
        /// <summary>
        /// OnException
        /// </summary>
        /// <param cref="ExceptionContext" name="context">Parameter for context</param>
        public override void OnException(ExceptionContext context)
        {
            if (context.Exception is CustomException)
            {
                context.HttpContext.Response.StatusCode = 500;
                context.Result = new ObjectResult(context.Exception.Message);
            }
        }
    }
}

كما ترون، نحن نلقي نظرة على ExceptionContext، عندما يكون الاستثناء نوعًا من CustomException، فإننا نفعل شيئًا ما.

يقوم هذا الشيء بتحديث الاستجابة ورمز الحالة لما سنعود إليه.

من أجل تحديث رمز الحالة، يجب علينا تحديث context.HttpContext.Response.StatusCode ومن أجل إرجاع النتيجة، علينا تحديث context.Result عن طريق إعطائه كائنًا موروثًا من ActionResult.

هذا مرشح، لذا فهذا يعني أنه يتعين علينا إضافته بشيء ما عن طريق إضافة [CustomExceptionFilter].

استخدام الفلتر

الآن، دعونا نكرر الطريقة التي لدينا ونضيف هذا الفلتر حتى يتم اتخاذ الإجراء اللازم، وستنتهي نقطة نهاية واجهة برمجة التطبيقات الخاصة بنا على النحو التالي:

using CustomExceptionHandleDemo.ExceptionFilterAttributes;
using CustomExceptionHandleDemo.Exceptions;
using Microsoft.AspNetCore.Mvc;

namespace CustomExceptionHandleDemo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        public WeatherForecastController()
        {
        }

        [HttpGet]
        [Route("GetWithoutExceptionHandler")]
        public Task GetWithoutExceptionHandler()
        {
            throw new CustomException("This is a custom exception!");
        }

        [HttpGet]
        [Route("GetWithExceptionHandler")]
        [CustomExceptionFilter]
        public Task GetWithExceptionHandler()
        {
            throw new CustomException("This is a custom exception!");
        }
    }
}

كما ترون، لدينا طريقة جديدة تسمى GetWithExceptionHandler، لها نفس المنطق الذي يمتلكه GetWithoutExceptionHandler، ولكن في هذه الحالة، أضفنا عامل التصفية [CustomExceptionFilter] إلى الطريقة.

والنتيجة هي التالية بعد تشغيل الطريقة، سأعرض صورة، لأنها لم تعد تظهر تتبع المكدس:

وبهذا أنشأنا استثناءً مخصصًا، وهو مرشح لتجاوز ما يحدث عندما نرمي واستثناء ونستخدمه في إحدى الطرق.

يمكن استخدام هذا للعديد من الأشياء مثل التسجيل ومعرفة ماذا ومتى وأين يحدث الخطأ.