[C#][ASP.NET] Web API 开发心得 (5) - 使用 Filter 统一 API 的回传格式和例外处理

今天要介绍的是 Filter,这是我非常喜欢的功能,Filter 有点像管道 Middleware 的延伸,会在管道结束后执行,Filter 的作用是,可在 Action 执行前执行后对 Request 进行加工处理,我们可以把一些通用的程式逻辑抽离,例如验证、例外处理和修改回传内容等等,在透过 Attribute 挂载到 Action 上,如此做可使 Action 更专注于本身的工作 关注点分离,独立的 Filter 模组更易于抽换和扩充,提高程式的内聚力和降低耦合度,让程式更好维护。

而在 Webform 时代我们如果想达到类似的功能,可以使用传统的 ASP.NET 管道模型,也就是 Middleware 的前身,在 ASP.NET 的管道中,Request 会经过多个 Module 最后才会抵达 Handler,Handler 就是我们比较常写的 ASPX 和 ASHX,Handler 结束后会再通过原来的 Module 原路返回,因此我们可以利用 Module 来对 Request 进行加工处理,不过没有办法像 Filter 一样灵活,这篇因为重点在 Filter 所以就不对 Module 多做介绍。

接下来要进入本篇的重点,在开发 API 时我们希望所有的 API 能有一致的输出格式,这样使用者就不需要为每个 API 去做不同的接收方式,让 API 使用起来更方便,Filter 可以分为下面三类,后面我会利用这三种不同的 Filter 来统一 API 的输出格式。

AuthorizationFilter: 在所有 Filter 之前执行,用于验证 Request 是否合法。ActionFilter: 里面有两个方法分别对应 Action 执行前和执行后。ExceptionFilter: 会在发生异常时执行。

Filter 流程图:
http://img2.58codes.com/2024/20106865PwacCSMKtL.jpg

上图蓝色箭头为 Request 经过 Filter 的正常流程,首先会经过 AuthorizationFilter,验证使用者资讯是否合法,接着通过 ActionFilter,最后到达 Action,结束后在经过 ActionFilter 返回,AuthorizationFilter 只有进来时才会执行,而橘色箭头是,如果在 ActionFilter 或 Action 内程式出现异常 Exception,那么就会被 ExceptionFilter 拦截做异常的处理,这里一定有人会问如果在 AuthorizationFilter 内出现异常呢,没错这里是需要注意的地方,我们不应该在 AuthorizationFilter 抛出异常,因为它不会被 ExceptionFilter 拦截处理。

统一的输出格式

新增 ResultViewModel 类别,作为我们所有 API 的统一介面,
类别内有下面三个属性:

success: 代表请求是否执行成功。msg: 存放异常的错误讯息。data: 存放请求成功后回传的资料。

程式码:

namespace ViewModel{    public class ResultViewModel    {        public bool success { get; set; }        public string msg { get; set; }        public object data { get; set; }    }}

新增 ResultAttribute 类别,继承 ActionFilter 覆写 OnActionExecuted 方法,该 Filter 的作用是包装 Action 回传的资料,将资料放入 ResultViewModel 的 data 属性内,再回传出去,这个 Filter 可以搭配 IgnoreResult Attribute 使用,如果我们希望有些 Controller 或 Action 的回传资料不要经过包装处理,例如档案下载,那么可以挂上 IgnoreResult 就会忽略这些 Action。

程式码:

namespace Filters{    public class ResultAttribute : ActionFilterAttribute    {        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)        {            if (actionExecutedContext.Exception != null)            {                return;            }            var ignoreResult1 = actionExecutedContext.ActionContext.ActionDescriptor.GetCustomAttributes<IgnoreResultAttribute>().FirstOrDefault();            var ignoreResult2 = actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<IgnoreResultAttribute>().FirstOrDefault();            if (ignoreResult1 != null || ignoreResult2 != null)            {                return;            }            var objectContent = actionExecutedContext.Response.Content as ObjectContent;            var data = objectContent?.Value;            var result = new ResultViewModel            {                success = true,                data = data            };            actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse(result);        }    }}

程式码:

namespace Filters{    public class IgnoreResultAttribute : Attribute    {    }}

例外处理

新增 CustomException 类别,有时候如果我们不希望回传的错误讯息,包含了敏感的资讯,例如执行SQL语法出错时,会回传部分的SQL语句,可能包含了栏位资讯或资料等等,因此就可以利用 CustomException 来识别这个 Exception 是不是已经被我们处理过,可否传到外部去。

程式码:

namespace Exceptions{    public class CustomException : Exception    {        public CustomException(string message) : base(message)        {        }    }}

新增 ExceptionAttribute 类别,继承 ExceptionFilter,该 Filter 会拦截异常,将错误讯息填入 ResultViewModel 的 msg 属性内,然后将包装后的异常回传,不过这里我并没有判断 Exception 是否是 CustomException,因为我还是习惯将所有异常讯息都回传,这个可以视专案需求调整。

程式码:

namespace Filters{    public class ExceptionAttribute : ExceptionFilterAttribute    {        public override void OnException(HttpActionExecutedContext actionExecutedContext)        {            var result = new ResultViewModel            {                success = false,                msg = actionExecutedContext.Exception.Message            };            actionExecutedContext.Response = actionExecutedContext.Request.CreateResponse(result);        }    }}

权限控制

新增 CustomAuthorizeAttribute 类别,继承 AuthorizationFilter,功能很单纯,仅用来判断使用者是否有登入,这里有使用到 上一篇 实作的 UserManager,而更细的身分判断我通常会写在 Action 内,Filter 只做第一层最简单的防护,CustomAuthorize 还可以搭配原 Web API 内建的 AllowAnonymous Attribute 使用,挂上这个 Attribute 的 Controller 或 Action 将不会执行验证权限的动作,代表这是个公开的 API,任何人都可以存取,Filter 内我有用 try catch 包住所有程式,就如同上面提到的,我们不应该在 AuthorizationFilter 内抛出异常,因为它不会被 ExceptionFilter 捕捉。

程式码:

namespace Filters{    public class CustomAuthorizeAttribute : AuthorizationFilterAttribute    {        protected readonly UserManager _userManager;        public CustomAuthorizeAttribute()        {            _userManager = new UserManager();        }        public override void OnAuthorization(HttpActionContext actionContext)        {            try            {                if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0)                {                    return;                }                if (actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0)                {                    return;                }                var user = _userManager.GetCurrentUser();                if (user == null)                {                    throw new CustomException("没有权限。");                }            }            catch (Exception ex)            {                if (!(ex is CustomException))                {                    ex = new CustomException("权限验证异常。");                }                var result = new ResultViewModel                {                    success = false,                    msg = ex.Message                };                actionContext.Response = actionContext.Request.CreateResponse(result);            }        }    }}

如何使用

我会新增一个 BaseController 并在 Controller 挂上 CustomAuthorize,然后让其它 Controller 继承,这样就可以让所有的 API 受到最基本的权限验证保护,而公开的 API 再加上 AllowAnonymous Attribute 关闭验证,这样做有个好处,可以防止未来新增 Action,或由别人来维护程式时忘记加上权限验证,导致资料外洩,接着新增一个 TestFilterController 并挂上 Exception 和 Result 来测试一下各个 Filter 的功能。

程式码:

namespace Api{    [CustomAuthorize]    public class BaseController : ApiController    {        public BaseController()        {        }    }}
namespace Api{    [Result]    [Exception]    [RoutePrefix("api/testFilter")]    public class TestFilterController : BaseController    {        //正常        [HttpGet]        [Route("getStudents_1")]        public List<Student> GetStudents_1()        {            return CreateStudents();        }        //忽略权限验证        [AllowAnonymous]        [HttpGet]        [Route("getStudents_2")]        public List<Student> GetStudents_2()        {            return CreateStudents();        }        //忽略 ResultFilter        [IgnoreResult]        [HttpGet]        [Route("getStudents_3")]        public List<Student> GetStudents_3()        {            return CreateStudents();        }        //抛出异常        [HttpGet]        [Route("getStudents_4")]        public List<Student> GetStudents_4()        {            throw new CustomException("取得资料失败");        }                private List<Student> CreateStudents()        {            return new List<Student>            {                new Student                {                    Id = 100,                    Name = "小明"                },                new Student                {                    Id = 101,                    Name = "小华"                },            };        }    }        public class Student    {        public int Id { get; set; }        public string Name { get; set; }    }}

资料夹结构
http://img2.58codes.com/2024/20106865Lq47GO5dp9.jpg

测试结果

未登入状态下对四个 API 的测试结果:

正常: api/testFilter/getStudents_1
http://img2.58codes.com/2024/20106865ki7e4NwwuD.jpg

忽略权限验证: api/testFilter/getStudents_2
http://img2.58codes.com/2024/20106865NgldgeyI4f.jpg

忽略 ResultFilter: api/testFilter/getStudents_3
http://img2.58codes.com/2024/20106865jss9JUGrPZ.jpg

抛出异常: api/testFilter/getStudents_4
http://img2.58codes.com/2024/20106865cu0nHGDYNz.jpg

登入状态下对四个 API 的测试结果:

正常: api/testFilter/getStudents_1
http://img2.58codes.com/2024/20106865GWZFtgHFxc.jpg

忽略权限验证: api/testFilter/getStudents_2
http://img2.58codes.com/2024/20106865oEPXpoHb1w.jpg

忽略 ResultFilter: api/testFilter/getStudents_3
http://img2.58codes.com/2024/20106865CjvNqZKxKu.jpg

抛出异常: api/testFilter/getStudents_4
http://img2.58codes.com/2024/201068657SNK15qaUO.jpg

结语

这篇我们使用 Filter 来统一 API 的回传格式和例外处理,除了方便使用者使用外,程式逻辑的抽离让 Action 能更专注于自己的工作,可自由组合的 Filter,让 API 面对各种不同需求时有更大的弹性,且将结果的处理拉到 Action 之外,才不会破坏原来的写法,依旧可由 Action 的回传型态看出 API 回传的资料结构,不会因为要统一格式,就造成 Action 都回传 ResultViewModel,程式的可读性变差。

今天就介绍到这里,感谢大家观看。

参考资料

使用Asp.Net MVC打造Web Api (16) - 统一输入/出格式以及异常处理策略
使用Asp.Net MVC打造Web Api (20) - 整合AOP功能
[铁人赛 Day14] ASP.NET Core 2 系列 - Filters


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章