dev-sect / ScheduleDevelopmentKit-backend

Инструмент для создания и работы с расписанием.
1 stars 0 forks source link

Глобальный хэндлер ошибок #10

Closed s4xack closed 2 years ago

s4xack commented 2 years ago

Нужно подключить глобальный хэндлер ошибок,

Bibletoon commented 2 years ago

Введение: я нашёл два варианта для отлова отлова ошибок

  1. С помощью RequestExceptionHandler из MediatR 2.С помощью IExceptionFilter из Asp

У каждого есть свой нюанс 1.Требует вернуть объект респонса, то есть у всех респонсов должен быть пустой конструктор или статик метод для создания 2.Просто выглядит не очень, т.к. для отлова конкретных ошибок их нужно ифать

Тут можно глянуть чуть более наглядно

В итоге есть какой-то третий путь или используем один из этих?

s4xack commented 2 years ago

Просто выглядит не очень, т.к. для отлова конкретных ошибок их нужно ифать

Ситуаций в, которых тебе нужно что-то конкретное отлавливать, да еще и на уровне всего приложения лучше всего избегать. Да и в целом хэндлинг ошибок зачастую нужен, тольчо чтобы не отдавать наверх внутренности каких-то сторонних либо, в остальных ситуаций ты грубо говоря хэндлишь все до факта самой ошибки (валидируешь аргументы перед вызовом БЛ, тыкаешь какие-то методы, которые говорят, что то или иное действие допустимо и т.д.) В тех ситуациях когда ошибка является "допустимым" сценарием, то есть какие-то ситуации валидность которых можно узнать только во время выполнения, хорошо бы использовать Result<T, E>. То есть твой доменный метод либо, при успешном завершениии, возвращает свой результат, либо, при каком-то ошибочном состоянии, отдает сущность ошибки. На уровне домена ошибкой может выступать, например иерархия рекордов, что-то типо:

public record CreateTeacherError
{
    public record SomeError(SomeErrorArg arg) :  CreateTeacherError;
}

На уровне хэндлера такой ошибкой будет некий ErrorDetails, некая моделька, которая для юзера является понятной ошибочкой с заголовком, текстом, статускодом и тд. Вот этот набор параметров подбирается согласно требованиям фронта.

Для резалтов уже придумали много разных штук типо FluentResults, часто вообще что-то самописное юзают с нужными хэлперами и тд

s4xack commented 2 years ago

Еще я немного позаимствовал статью с работы и засунул ее в md, тут какжется более развернуто и точно сказано о том, что я хотел донести

Тык # Обработка ошибок ## Ошибки можно разделить на два типа: - Ошибки, c которыми мы знаем, что делать и как их обработать (неисключительные ситуации\ошибки) - Ошибки, про которые мы не знаем, как их обработать (исключительные ошибки) Зачастую для обоих типов ошибок применяются исключения (Exception). Плюс такого подхода в универсальности и однообразии, исключения можно одинаково применять в различных ситуациях: валидация, проверка аргументов методов, нарушение бизнес-правил, инфраструктурные проблемы, например недоступен сервер БД, и т.д. Исключение можно создать на любом уровне кода, при этом произойдёт быстрый выход из метода и переход выполнения в блок catch на том же уровне или выше. ### Пример удаления активности из тренировки: Удаление активности из тренировки ```csharp // контроллер class TrainingController { public ActionResult DeleteActivity(int trainingId, int activityId) { try { _mediator.Send(new DeleteActivityCommand(trainingId, activityId)); return Ok(); } // обработка может быть вынесена из контроллера выше, в Middleware или Exception Filter catch (DomainException e) { return Problem(title: e.Title, details: e.Details); } } } // хэндлер команды class DeleteActivityHandler { public void Handle(DeleteActivityCommand command) { var training = _trainingRepository.GetById(command.TrainingId); var activity = _activityRepository.GetById(command.ActivityId); training.DeleteActivity(activity); } } // домен class Training { public void DeleteActivity([NotNull] Activity activity) { if (activity == null) throw new ArgumentNullException(nameof(activity)); if (activity.Training != this) throw new ArgumentException("Training work does not belong to the training.", nameof(activity)); // бизнес-правило: if (activity.Marks.Count != 0) { throw new DomainException("Удаление активности", "Невозможно удалить активность, так как есть выставленные оценки.") // или более конкретный тип исключения: // throw new DeleteActivityException("Невозможно удалить активность, так как есть выставленные оценки."); } Data.Activities.Children.Remove(activity); } } ``` Несмотря на кажущуюся простоту и удобство, у исключений есть несколько проблем: - По сигнатуре невозможно определить возможные исключения при вызове метода - В общем случае затрудняется чтение и понимание кода: - Внутри секции try необходимо помнить про альтернативный поток выполнения в блоке catch, либо в нескольких блоках при обработке разных видов ошибок; - Если про метод известно, что в нём может быть создано исключение, но при этом в вызывающем коде этот метод не завёрнут в try\catch, возникает вопрос: это сделано намеренно или разработчик просто забыл обработать ошибку? Если намеренно, то на каком уровне выше будет обработка этого исключения? Для того, чтобы это понять, надо вручную пройти по стеку вызовов методов. В примере выше эта проблема наблюдается в обработчике команды. ## Когда использовать исключения? Исключения нужны для исключительных ситуаций. То есть для ситуаций, которые мы не ожидаем и с которыми не знаем что делать. При этом единственный правильный вариант - это прервать выполнение текущей операции\метода. Примеры исключительных ситуаций: ошибка подключения к БД; отсутствие параметра конфигурации; нарушение контракта (в примере выше - проверка активности на null). Валидация не относится к исключительным ситуациям, так как мы ожидаем, что на вход могут прийти некорректные данные, которые необходимо проверить и обработать явно. Является ли ситуация исключительной также зависит от контекста. Например, если мы ожидаем, что какой-нибудь внешний сервис может быть кратковременно недоступен, мы можем обработать ошибку подключения и показать пользователю понятное сообщение иои повторить попытку. Но если после нескольких попыток запрос к сервису так и не выполнился, это уже становится исключительной ситуацией, в которой необходимо либо пробросить исключение от внешнего сервиса наверх, либо обернуть его в собственное исключение. При этом не следует отлавливать все исключения, например используя catch (Exception ex) . Необходимо обрабатывать минимальный набор типов исключений, которые мы можем обработать безопасно. Иначе есть риск некорректно обработать неожидаемое исключение, потерять информацию об ошибке, и оставить систему в неконсистентном состоянии. Как вернуть ошибочный результат? Если в неисключительных (ожидаемых) ситуациях требуется вернуть результат выполнения или ошибку, можно использовать класс Result (в некоторых функциональных языках - Either ): ```csharp public class Result { public bool IsSuccess { get; } public bool IsFailure { get; } public TError Error => _error; public TValue Value => IsSuccess ? _value : throw new InvalidOperationException("Failure result has no Value"); private Result() {/* ... */} // методы для создания результата public static Result Success(TValue value) {/* ... */} public static Result Failure(TError error) {/* ... */} } ``` Результат может содержать значение в случае успеха или ошибку, но не то и другое одновременно. Класс Result реализован в проекте Dnevnik.Sport.Shared , где также содержатся различные методы-хелперы для работы с результатом. Изначальный пример с удалением активности можно переписать следующим образом: ```csharp // домен class Training { // атрибут MustUseReturnValue вызовет ошибку компиляции, если забыть обработать результат вызова метода [MustUseReturnValue] public Result DeleteActivity([NotNull] Activity activity) { // используются исключения, так как это контракты метода if (activity == null) throw new ArgumentNullException(nameof(activity)); if (activity.Training != this) throw new ArgumentException("Training work does not belong to the training.", nameof(activity)); if (activity.Marks.Count != 0) { // более длинный и явный вариант: // return Result.Failure(new DeleteActivityError.MarksSet()); // // класс Result содержит безопасные неявные приведения типов TValue -> Result и TError -> Result // поэтому можно просто вернуть значение ошибки return new DeleteActivityError.MarksSet(); // return new DeleteActivityError.MarksSetWithCount(activity.Marks.Count); } Data.Activities.Children.Remove(activity); // Так как мы не можем объявить тип Result, то используем функциональный аналог void - Unit // Этот тип можно использовать в тех случаях, когда успешный сценарий не возвращает какое-либо значение return Unit.Instance; } // Из домена не следует возвращать пользовательский текст, так как это ответственность слоя приложения. // Необходимо возвращать код ошибки или тип, определяющий ошибку. // // Для значений ошибок принято решение использовать record'ы, как удобную и менее громоздкую альтернативу классам. // Другой вариант - использовать enum, но плюс record'а в том, что в нём могут содержаться дополнительные данные. Пример: MarksSetWithCount. public abstract record DeleteActivityError { public record MarksSet : DeleteActivityError; public record MarksSetWithCount(int marksCount) : DeleteActivityError; } } // хэндлер команды class DeleteActivityHandler { // Явно возвращаем из обработчика результат выполнения. // Класс ErrorDetails объявлен в неймспейсе Dnevnik.Sport.Infrastructure.ErrorHandling и представляет собой аналог ProblemDetails для возврата из WebAPI. public Result Handle(DeleteActivityCommand command) { var training = _trainingRepository.GetById(command.TrainingId); var activity = _activityRepository.GetById(command.ActivityId); // НЕВЕРНО: вызовет ошибку компиляции, так как не обрабатывается результат операции training.DeleteActivity(activity); // ВЕРНО: var deleteResult = training.DeleteActivity(activity); if (deleteResult.IsFailure) { return deleteResult.Error switch { Training.DeleteActivityError.MarksSet => new ErrorDetails("Ошибка при удалении", "Есть связанные данные по событию (отметки и/или оценки)"), Training.DeleteActivityError.MarksSetWithCount(var marksCount) => new ErrorDetails("Ошибка при удалении", $"Есть связанные данные по событию (отметки и/или оценки). Всего событий: {marksCount}"), _ => throw new InvalidOperationException(), } } return Unit.Instance; } } // контроллер class TrainingController { public ActionResult DeleteActivity(int trainingId, int activityId) { var result = _mediator.Send(new DeleteActivityCommand(trainingId, activityId)); return result.IsSuccess ? Ok() : Problem(title: result.Error.Title, details: result.Error.Details); // Либо можно воспользоваться методом FromResult из базового контроллера. // Метод либо возвращает Ok с успешным значением результата, либо Problem с деталями из ошибки результата и указанным статус-кодом: return FromResult(result, StatusCodes.Status422UnprocessableEntity); } } ```
s4xack commented 2 years ago

TL;DR; Кажется что сейчас достаточно обычного IExceptionFilter, который будет мапить ошибку в простенькую модель с месджом, стэктрейсом и т.д.