Обработка исключительных ситуаций ASP.NET Core Web API

Я начал использовать ASP.NET Core для моего нового проекта API REST после использования обычного ASP.NET Web API в течение многих лет. Я не вижу хорошего способа обработки исключений в ASP.NET Core Web API. Я попытался реализовать фильтр/атрибут обработки исключений:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

И вот регистрация моего автозагрузки:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

Проблема, с которой я столкнулась, заключается в том, что при возникновении исключения в моем AuthorizationFilter это не обрабатывается ErrorHandlingFilter. Я ожидал, что его поймают там, как будто он работал со старым ASP.NET Web API.

Итак, как я могу уловить все исключения приложений, а также любые исключения из Action Filters?

Ответ 1

Промежуточное ПО для обработки исключений

После многих экспериментов с различными подходами к обработке исключений я в конечном итоге использовал промежуточное ПО. Лучше всего он подошел для моего приложения ASP.NET Core Web API. Он обрабатывает исключения приложений, а также исключения из фильтров действий, и у меня есть полный контроль над обработкой исключений и ответом HTTP. Вот мое промежуточное ПО для обработки исключений:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

Зарегистрируйте его до MVC в классе Startup:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

Вы можете добавить трассировку стека, имя типа исключения, коды ошибок или все, что вы хотите к нему. Очень гибкий Вот пример ответа об исключении:

{ "error": "Authentication token is not valid." }

Рассмотрите возможность внедрения IOptions<MvcJsonOptions> в метод Invoke чтобы затем использовать его при сериализации объекта ответа для использования параметров сериализации ASP.NET MVC в JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings) для лучшей согласованности сериализации во всех конечных точках.

Подход 2

Есть еще один неочевидный API, называемый UseExceptionHandler который работает "хорошо" для простых сценариев:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = feature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

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

Ответ 2

Лучше всего использовать Middleware для достижения требуемого ведения журнала. Вы хотите, чтобы ваше исключение регистрировалось в одном промежуточном программном обеспечении, а затем обрабатывало страницы ошибок, отображаемые пользователю в другом промежуточном программном обеспечении. Это позволяет разделить логику и следовать дизайну, разработанному Microsoft с помощью двух промежуточных компонентов. Здесь хорошая ссылка на документацию Microsoft: Обработка ошибок в ASP.Net Core

В вашем конкретном примере вы можете использовать одно из расширений в промежуточном программном обеспечении StatusCodePage или сворачивать свой собственный, например это.

Здесь вы можете найти пример для регистрации исключений: ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

Если вам не нравится эта конкретная реализация, вы также можете использовать ELM Middleware, и вот несколько примеров: Elm Exception Middleware

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

Если это не сработает для ваших нужд, вы всегда можете использовать свой собственный компонент Middleware, просматривая их реализации ExceptionHandlerMiddleware и ElmMiddleware, чтобы понять концепции для создания своих собственных.

Важно добавить промежуточное ПО для обработки исключений под промежуточным программным обеспечением StatusCodePages, но прежде всего ваши другие компоненты промежуточного программного обеспечения. Таким образом, ваше промежуточное программное обеспечение Exception захватит исключение, зарегистрирует его, а затем разрешит запросу перейти к промежуточному программному обеспечению StatusCodePage, которое отобразит пользователю страницу с дружественной ошибкой.

Ответ 3

В Asp.Net Core (по крайней мере из 2.2, возможно, раньше) имеется встроенное промежуточное ПО, которое делает его немного проще по сравнению с реализацией в принятом ответе:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Он должен делать почти то же самое, просто немного меньше кода для написания. Не забудьте добавить его перед UseMvc как порядок важен.

Ответ 4

Чтобы настроить поведение обработки исключений для типа исключения, вы можете использовать Middleware из пакетов NuGet:

Пример кода:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}

Ответ 5

Хорошо принятый ответ мне очень помог, но я хотел передать HttpStatusCode в свое промежуточное ПО для управления кодом состояния ошибки во время выполнения.

По этой ссылке у меня появилась идея сделать то же самое. Так что я слил Андрей Ответ с этим. Итак, мой окончательный код ниже:
1. Базовый класс

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2. Пользовательский тип класса исключения

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3. Промежуточное ПО на заказ

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4. Метод продления

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5. Настройте метод в файле startup.cs.

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Теперь мой метод входа в систему в учетной записи контроллера:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }

Выше вы можете увидеть, что если я не нашел пользователя, то поднял исключение HttpStatusCodeException, в котором я передал статус HttpStatusCode.NotFound и пользовательское сообщение.
В промежуточном программном обеспечении

catch (HttpStatusCodeException ex)

будет вызван заблокированный, который передаст управление

метод приватной задачи HandleExceptionAsync (контекст HttpContext, исключение HttpStatusCodeException)




Но что, если я получил ошибку во время выполнения раньше? Для этого я использовал блок try catch, который выдает исключение и будет перехвачен в блоке catch (Exception exceptionObj) и передаст управление

Task HandleExceptionAsync (контекст HttpContext, исключение исключения)

метод.

Я использовал один класс ErrorDetails для единообразия.

Ответ 6

Во-первых, благодаря Андрею, поскольку я основывал свое решение на его примере.

Я включаю в себя мой, поскольку это более полный образец и может сэкономить читателям некоторое время.

Ограничение подхода Andrei заключается в том, что он не обрабатывает ведение журнала, захватывает потенциально полезные переменные запроса и согласование контента (он всегда будет возвращать JSON независимо от того, что клиент запросил - XML /обычный текст и т.д.).

Мой подход заключается в использовании ObjectResult, который позволяет нам использовать функциональность, запеченную в MVC.

Этот код также предотвращает кэширование ответа.

Ответ на ошибку был оформлен таким образом, что его можно сериализовать с помощью сериализатора XML.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}

Ответ 7

Сначала настройте ASP.NET Core 2 Startup для повторного выполнения на странице ошибки для любых ошибок с веб-сервера и любых необработанных исключений.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

Затем определите тип исключения, который позволит вам вызывать ошибки с кодами состояния HTTP.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Наконец, в вашем контроллере для страницы с ошибкой настройте ответ на основе причины ошибки и будет ли ответ отображаться непосредственно конечным пользователем. Этот код предполагает, что все URL-адреса API начинаются с /api/.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

Ядро ASP.NET регистрирует деталь ошибки, которую вы должны отлаживать, поэтому код состояния может быть всем, что вы хотите предоставить (потенциально ненадежному) реквестером. Если вы хотите показать дополнительную информацию, вы можете улучшить HttpException, чтобы обеспечить ее. Для ошибок API вы можете поместить JQT-кодированную информацию об ошибке в тело сообщения, заменив return StatusCode... на return Json....

Ответ 8

используйте промежуточное ПО или IExceptionHandlerPathFeature - это нормально. В интернет-магазине есть другой путь

создать исключительный фильтр и зарегистрировать его

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})