在 ASP.NET Core 中使用中間件處理異常

前言:

本文使用 .NET Core SDK 3.1 的版本,介紹了處理 ASP.NET Core Web 應用中常見異常的一些方法。
本文 Demo 中貫穿全文的代碼如下:

#region Enums
    public enum ResultState
    {
        請求成功 = 200, 未登錄 = 300, 業務錯誤 = 500, 未知錯誤 = 999  
    }
#endregion

#region Exceptions
    public interface IKnownException
    {
        public ResultState Code { get; set; }
        public string Message { get; }
        public object Result { get; }
    }
    
    public class KnownException : Exception, IKnownException
    {
        public KnownException(string message) : base(message) { }

        public KnownException(string message, object result) : base(message) { Result = result; }

        public ResultState Code { get; set; }

        public object Result { get; }
    }

    public class KnownExceptionMessage : IKnownException
    {
        public ResultState Code { get; set; }

        public string Message { get; private set; }

        public object Result { get; private set; }

        public readonly static IKnownException UnKnown = new KnownExceptionMessage { Message = "未知錯誤", Code = ResultState.未知錯誤 };

        public IKnownException FromKnownException(IKnownException knownException)
        {
            return new KnownExceptionMessage { Code = knownException.Code, Message = knownException.Message, Result = knownException.Result };
        }
    }
#endregion

一、開發人員異常頁

開發人員異常頁 顯示請求異常的詳細信息。
Startup.Configure 方法添加代碼,以當應用在開發環境中運行時啓用此頁:

    if (env.IsDevelopment())
    {
        // 開發人員異常頁
        app.UseDeveloperExceptionPage();
    }

根據中間件管道的順序,將 UseDeveloperExceptionPage` 調用置於要捕獲其異常的任何中間件前面。
僅當應用程序在開發環境中運行時才啓用開發人員異常頁 。

二、異常處理程序頁

爲生產環境配置自定義錯誤處理頁,使用異常處理中間件。
使用 UseExceptionHandler 在非開發環境中添加異常處理中間件:

    if (env.IsDevelopment())
        app.UseDeveloperExceptionPage();
    else
        app.UseExceptionHandler("/Error");

異常的 Controller 如下:

    [ApiController]
    public class ErrorController : ControllerBase
    {
        private readonly JsonSerializerOptions jsonSerializerOptions;

        public ErrorController(IOptionsMonitor<JsonOptions> jsonOptins)
        {
            jsonSerializerOptions = jsonOptins.CurrentValue.JsonSerializerOptions;
        }

        [Route("Error")]
        [AllowAnonymous]
        public async Task Error()
        {
            var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
            // var originalRequestPath = exceptionHandlerPathFeature?.Path; // 原始路徑
            var knownException = exceptionHandlerPathFeature.Error as IKnownException; // 
            if (knownException == null)
            {
            	// 未知異常 Http 響應碼 500
                HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
            }
            else
            {
                knownException.Code = ResultState.業務錯誤;
                // 業務邏輯異常 Http 響應碼 200 
                HttpContext.Response.StatusCode = StatusCodes.Status200OK;
            }
            HttpContext.Response.ContentType = "application/json;";
            await HttpContext.Response.WriteAsync(
                JsonSerializer.Serialize(knownException, jsonSerializerOptions), Encoding.UTF8);
        }
    }

使用 [AllowAnonymous] 允許匿名訪問,未身份驗證也能夠訪問。
上面代碼使用 IExceptionHandlerPathFeature 訪問上下文報出的異常信息和原始路徑。
如果我們使用以下代碼:

    [HttpGet]
    public int[] Index()
    {
        throw new KnownException("this is message", "this is result");
        return new int[] { 1, 2, 3 };
    }

前端將得到如下JSON:

	{"code":500,"message":"this is message","result":"this is result"}

如果返回值中有中文可能會被轉換爲 Unicode,請修改 JsonSerializerOptionsEncoder 屬性爲 JavaScriptEncoder.UnsafeRelaxedJsonEscaping

#region Startup.ConfigureServices
    services.AddControllers()    // services.AddMvc()
        .AddJsonOptions(jsonOptions =>
        {
            jsonOptions.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
        });
#endregion
or
#region
    var jsonSerializerOptions = context.RequestServices.GetService<IOptionsMonitor<JsonOptions>>()
        .CurrentValue.JsonSerializerOptions;
	jsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;    
#endregion

三、異常處理程序 lambda

異常處理程序頁 的替代方法可以向 UseExceptionHandler 提供 lambda,也可以在相應前訪問上下文報出的異常。

    app.UseExceptionHandler(errorApp =>
    {
        errorApp.Run(async context =>
        {
            var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
            // var originalRequestPath = exceptionHandlerPathFeature?.Path;
            var knownException = exceptionHandlerPathFeature.Error as IKnownException;
    
            if (knownException == null)
            {
            	// 未知異常 Http 響應碼 500
                context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            }
            else
            {
                knownException.Code = ResultState.業務錯誤;
                // 業務邏輯異常 Http 響應碼 200 
                context.Response.StatusCode = StatusCodes.Status200OK;
            }
    
            var jsonSerializerOptions = context.RequestServices.GetService<IOptionsMonitor<JsonOptions>>()
                .CurrentValue.JsonSerializerOptions;
            context.Response.ContentType = "application/json;";
            await context.Response.WriteAsync(
                JsonSerializer.Serialize(knownException, jsonSerializerOptions), Encoding.UTF8);
        });
    });

四、異常過濾器 IExceptionFilter

異常過濾器只作用於 MVC 的生命週期,
如果我們需要對 Controller 進行特殊異常處理,對整體來講又需要用整體的異常出來,可以用 IExceptionFilter

1) 直接實現 IExceptionFilter 的方式

#region Startup.ConfigureServices
    services.AddControllers(mvcOptions =>
    {
        mvcOptions.Filters.Add<MyExceptionFilter>();
    }).AddJsonOptions(jsonOptions =>
    {
    	// 如果返回值中有中文可能會被轉換爲 Unicode 。
        jsonOptions.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
    });
#endregion

#region MyExceptionFilter
    public class MyExceptionFilter : IExceptionFilter
    {
        public void OnException(ExceptionContext context)
        {
            var knownException = context.Exception as IKnownException;
            if (knownException == null)
            {
                knownException = KnownExceptionMessage.UnKnown;
                context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
            }
            else
            {
                knownException.Code = ResultState.業務錯誤;
                knownException = new KnownExceptionMessage().FromKnownException(knownException);
                context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
            }

            context.Result = new JsonResult(knownException)
            {
                ContentType = "application/json; charset=utf-8"
            };
        }
    }
#endregion

2) 繼承 ExceptionFilterAttribute 的方式

ExceptionFilterAttribute 也實現了 IExceptionFilter接口。
寫法和 直接實現 IExceptionFilter 的方式 一樣的

    public class MyExceptionFilterAttribute : ExceptionFilterAttribute
    {
        public override void OnException(ExceptionContext context)
        {
            var knownException = context.Exception as IKnownException;
            if (knownException == null)
            {
                knownException = KnownExceptionMessage.UnKnown;
                context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
            }
            else
            {
                knownException.Code = ResultState.業務錯誤;
                knownException = new KnownExceptionMessage().FromKnownException(knownException);
                context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
            }

            context.Result = new JsonResult(knownException)
            {
                ContentType = "application/json; charset=utf-8"
            };
        }
    }

在使用時,我們可以直接攔截所有的 Controller 的錯誤:

    services.AddControllers(mvcOptions =>
    {
        mvcOptions.Filters.Add<MyExceptionFilterAttribute>();
    });

也可以使用註解的方式單獨攔截某個 Controller

    [ApiController]
    [MyExceptionFilter]
    public class HomeController : ControllerBase { /* Do something... */}


參考文檔

處理 ASP.NET Core 中的錯誤

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章