在 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 中的错误

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