web-dev-qa-db-ja.com

ASP.NET Core Web APIの例外処理

何年もの間通常のASP.NET Web APIを使用した後、私は新しいREST AP​​IプロジェクトにASP.NET Coreを使用し始めました。 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と同じように機能することを期待していました。

それでは、どのようにしてすべてのアプリケーション例外とアクションフィルタからの例外をキャッチすることができますか?

185
Andrei

例外処理ミドルウェア

さまざまな例外処理アプローチを用いた多くの実験の後、私はミドルウェアを使用しました。それは私のASP.NET Core Web APIアプリケーションに最適でした。これはアプリケーション例外とフィルタからの例外を処理します。私は例外処理と応答jsonの作成を完全に制御しています。これが私の例外処理ミドルウェアです。

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);
    }
}

StartupクラスにMVCより前を登録します。

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

スタックトレース、例外タイプ名、エラーコード、その他必要なものを追加できます。とても柔軟です。これが例外応答の例です。

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

すべてのエンドポイントでシリアル化の一貫性を向上させるために、JsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)でASP.NET MVCのシリアル化設定を利用するために応答オブジェクトをシリアル化するときにIOptions<MvcJsonOptions>Invokeメソッドに注入して使用することを検討してください。

アプローチ2

もう1つのUseExceptionHandlerというAPIがあり、これによって作業が少し楽になります。

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);
}));

これは非常に明白ではありませんが、例外処理を設定する簡単な方法です。ただし、必要な依存関係を注入する機能を使用して制御を強化するため、ミドルウェアによるアプローチを優先しています。

380
Andrei

最善の策は、ミドルウェアを使用して、探しているロギングを達成することです。あなたはあなたの例外ロギングを一つのミドルウェアに入れて、それから別のミドルウェアでユーザに表示されたあなたのエラーページを処理したいです。これにより、ロジックを分離することができ、Microsoftが2つのミドルウェアコンポーネントで設計した設計に従います。これはMicrosoftのドキュメントへの良いリンクです: ASP.Net Coreでのエラー処理

あなたの具体的な例では、 StatusCodePageミドルウェア にある拡張機能の1つを使用するか、または this のように独自に拡張することができます。

あなたは例外をログに記録するための例をここに見つけることができます: 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();
}

それでもうまくいかない場合は、ExceptionHandlerMiddlewareおよびElmMiddlewareの実装を調べて独自のミドルウェアコンポーネントを作成し、独自のものを構築するための概念を把握することができます。

StatusCodePagesミドルウェアの下、ただし他のすべてのミドルウェアコンポーネントの上に、例外処理ミドルウェアを追加することが重要です。そのようにして、あなたのExceptionミドルウェアは例外を捕獲し、それを記録し、そしてリクエストがStatusCodePageミドルウェアに進むことを可能にし、それはユーザーに分かりやすいエラーページを表示するでしょう。

24
Ashley Lee

よく受け入れられた答えは私を大いに助けました、しかし、私は実行時にエラーステータスコードを管理するために私のミドルウェアでHttpStatusCodeを渡したかったです。

このリンク によると、私は同じことをするためのアイデアを得ました。だから私はこれとAndrei Answerをマージしました。だから私の最終的なコードは以下の通りです:
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でMethodを設定します

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;
        }

上記の私はユーザーを見つけていないかどうかを見ることができます私はHttpStatusCode.NotFoundステータスとカスタムメッセージを渡したHttpStatusCodeExceptionを発生させる
ミドルウェア

catch(HttpStatusCodeException ex)

ブロックされたものが呼び出され、そこに制御が渡されます。

private Task HandleExceptionAsync(HttpContextコンテキスト、HttpStatusCodeException例外)メソッド




しかし、以前にランタイムエラーが発生した場合はどうなりますか?そのために私は例外をスローしてcatch(Exception exceptionObj)ブロックにキャッチされ、に制御を渡すtry catchブロックを使用しています

タスクHandleExceptionAsync(HttpContextコンテキスト、例外例外)

方法。

統一性のために単一のErrorDetailsクラスを使用しました。

14
Arjun

例外タイプごとに例外処理動作を設定するには、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();
}
14
Ihar Yakimush

最新の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の前に追加するのを忘れないでください。

14

最初に、私の解決策を彼の例に基づいたAndreiに感謝します。

私はそれをより完全なサンプルであるので私は含めます、そして読者をしばらく時間を節約するかもしれません。

Andreiのアプローチの限界はロギングを扱わず、潜在的に有用なリクエスト変数とコンテンツネゴシエーションを捕獲することです(クライアントが何をリクエストしたとしても常にJSONを返します - XML /プレーンテキストなど)。

私のアプローチは、MVCに組み込まれた機能を使えるようにするObjectResultを使うことです。

このコードはレスポンスのキャッシュも防ぎます。

エラー応答は、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;
        }
    }
}
12
CountZero

まず、Webサーバーからのエラーや未処理の例外が発生した場合に、エラーページに再実行するように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; }
}

最後に、エラーページのコントローラーで、エラーの理由と、その応答がエンドユーザーに直接表示されるかどうかに基づいて応答をカスタマイズします。このコードでは、すべてのAPI URLが/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 Coreはデバッグのためにエラーの詳細をログに記録するので、ステータスコードだけが(潜在的に信頼されていない)リクエスタに提供したい場合があります。より多くの情報を表示したい場合は、それを提供するためにHttpExceptionを拡張することができます。 APIエラーの場合は、return StatusCode...return Json...に置き換えることで、JSONエンコードのエラー情報をメッセージ本文に入れることができます。

7
Edward Brey