Пользовательская обработка исключений в .NET API

· 3 мин чтения

Исключения – это плохо, мы знаем, верно? Но что, если нам придется с ними справиться?

Что происходит, когда у нас есть исключение, например в API, оно отображает сообщение стека, содержащее много информации, которую мы, возможно, захотим удалить из ответа, который получают наши пользователи.

Для демонстрации я создал API dotnet и добавил метод, который выдает исключение:

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

Если мы создадим исключение, это будет выглядеть так:

[[[ТОК_1]]]

Это выглядит нормально, это то, что вы получаете из исключения, но для меня это отображение большого количества информации. Если у вас есть библиотеки и прочее, оно может показать полезную информацию о вашем клиенте, вашем проекте или других вещах тому, кто может пытаться что-то увидеть.

Вот как это выглядит на Swagger:

Создание собственного исключения

Этого шага можно было бы избежать, поскольку мы знаем, какое исключение будет выдано, в данном случае Exception, но для меня лучше иметь собственные исключения, поскольку у вас больше контроля над тем, что вы выбрасываете.

В данном случае я только что создал объект CustomException, который наследуется от Exception:

[[[ТОК_6]]]

После того, как мы создали собственное исключение, давайте обновим наш метод, чтобы он выдавал CustomException вместо Exception:

[[[ТОК_9]]]

На данный момент это ничего не изменит, но трассировка стека покажет, что созданный объект является CustomException вместо Exception, взгляните на начало трассировки стека:

[[[ТОК_12]]]

Создание атрибута 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].

Использование фильтра

Теперь давайте воспроизведем имеющийся у нас метод и добавим этот фильтр, чтобы он действовал. Конечная точка нашего API будет выглядеть следующим образом:

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] к методу.

Результат следующий: после запуска метода я отобразю изображение, поскольку оно больше не показывает трассировку стека:

Таким образом, мы создали собственное исключение, фильтр для переопределения того, что происходит, когда мы выбрасываем исключение и используем его в методе.

Это можно использовать для множества вещей, таких как ведение журнала и знание того, что, когда и где происходит ошибка.