在Asp.Net Core中使用ModelConvention实现全局过滤器隔离
从何说起
这来自于我把项目迁移到Asp.NetCore的过程中碰到一个问题。在一个web程序中同时包含了MVC和WebAPI,现在需要给WebAPI部分单独添加一个接口验证过滤器IActionFilter,常规做法一般是写好过滤器后给需要的控制器挂上这个标签,高级点的做法是注册一个全局过滤器,这样可以避免每次手动添加同时代码也更好管理。注册全局过滤器的方式为:
services.AddMvc(options=> { options.Filters.Add(typeof(AccessControlFilter)); });
但这样做会带来一个问题,那就是MVC部分控制器也会受影响,虽然可以在过滤器中进行一些判断来区分哪些是MVCController哪些是APIController,但是平白无故给MVC增加这么一个没用的Filter,反正我是不能忍,所以寻找有没有更好的办法来实现这个功能。
于是ModelConvention(可以翻译为模型约定)闪亮登场。
先认识下ApplicationModel
看一下官方文档是怎么描述应用程序模型(ApplicationModel)的:
ASP.NETCoreMVCdefinesanapplicationmodelrepresentingthecomponentsofanMVCapp.YoucanreadandmanipulatethismodeltomodifyhowMVCelementsbehave.Bydefault,MVCfollowscertainconventionstodeterminewhichclassesareconsideredtobecontrollers,whichmethodsonthoseclassesareactions,andhowparametersandroutingbehave.Youcancustomizethisbehaviortosuityourapp'sneedsbycreatingyourownconventionsandapplyingthemgloballyorasattributes.
简单一点说,ApplicationModel描述了MVC应用中的各种对象和行为,这些内容包含Application、Controller、Action、Parameter、Router、Page、Property、Filter等等,而Asp.NetCore框架本身内置一套规则(Convention)用来处理这些模型,同时也提供了接口给我们自定义约定来扩展模型以实现更符合需要的应用。
和应用程序模型有关的类都定义在命名空间Microsoft.AspNetCore.Mvc.ApplicationModels中,这些模型通过IApplicationModelProvider构建出来,Asp.NetCore框架提供的默认Provider是DefaultApplicationModelProvider。我们可以编辑这些模型,从而更改它的表现行为,这就要借助它的ModelConvention来实现。
ModelConvention
ModelConvention定义了操作模型的入口,又或者说是一种契约,通过它我们可以对模型进行修改,常用的Convention包括:
- IApplicationModelConvention
- IControllerModelConvention
- IActionModelConvention
- IParameterModelConvention
- IPageRouteModelConvention
这些接口提供了一个共同的方法Apply,方法参数是各自的应用程序模型,以IControllerModelConvention为例看一下它的定义:
namespaceMicrosoft.AspNetCore.Mvc.ApplicationModels { // //摘要: //AllowscustomizationoftheMicrosoft.AspNetCore.Mvc.ApplicationModels.ControllerModel. // //言论: //Tousethisinterface,createanSystem.Attributeclasswhichimplementsthe //interfaceandplaceitonacontrollerclass.Microsoft.AspNetCore.Mvc.ApplicationModels.IControllerModelConvention //customizationsrunafterMicrosoft.AspNetCore.Mvc.ApplicationModels.IApplicationModelConvention //customizationsandbeforeMicrosoft.AspNetCore.Mvc.ApplicationModels.IActionModelConvention //customizations. publicinterfaceIControllerModelConvention { // //摘要: //CalledtoapplytheconventiontotheMicrosoft.AspNetCore.Mvc.ApplicationModels.ControllerModel. // //参数: //controller: //TheMicrosoft.AspNetCore.Mvc.ApplicationModels.ControllerModel. voidApply(ControllerModelcontroller); } }
从接口摘要可以看到,这个接口允许自定义ControllerModel对象,而如何自定义内容正是通过Apply方法来实现,这个方法提供了当前ControllerModel对象的实例,我们可以在它身上获取到的东西实在太多了,看看它包含些什么:
有了这些,我们可以做很多很灵活的操作,例如通过设置ControllerName字段强制更改控制器的名称让程序中写死的控制器名失效,也可以通过Filters字段动态更新它的过滤器集合,通过RouteValues来更改路由规则等等。
说到这里,很多人会觉得这玩意儿和自定义过滤器看起来差不多,最开始我也这么认为,但经过实际代码调试我发现它的生命周期要比过滤器早的多,或者说根本无法比较,这个家伙只需要在应用启动时执行一次并不用随着每次请求而执行。也就是说,它的执行时间比激活控制器还要早,那时候根本没有过滤器什么事儿,它的调用是发生在app.UseEndpoints()。
回到最开始的需求。基于上面的介绍,我们可以自定义如下的约定:
publicclassApiControllerAuthorizeConvention:IControllerModelConvention { publicvoidApply(ControllerModelcontroller) { if(controller.Filters.Any(x=>xisApiControllerAttribute)&&!controller.Filters.Any(x=>xisAccessControlFilter)) { controller.Filters.Add(newAccessControlAttribute()); } } }
上面的主要思路就是通过判断控制器本身的过滤器集合是否包含ApiControllerAttribute来识别是否APIController,如果是APIController并且没有标记过AccessControlAttribute的话就新建一个实例加入进去。
那么如何把这个约定注册到应用中呢?在Microsoft.AspNetCore.Mvc.MvcOptions中提供了Conventions属性:
// //摘要: //GetsalistofMicrosoft.AspNetCore.Mvc.ApplicationModels.IApplicationModelConvention //instancesthatwillbeappliedtotheMicrosoft.AspNetCore.Mvc.ApplicationModels.ApplicationModel //whendiscoveringactions. publicIListConventions{get;}
通过操作它就能把自定义约定注入进去:
services.AddMvc(options=> { options.Conventions.Add(newApiControllerAuthorizeConvention()); })
细心的人会发现,Conventions是一个IApplicationModelConvention类型的集合,而我们自定义的Convention是一个IControllerModelConvention,正常来说应该会报错才对?原因是Asp.NetCore的DI框架帮我们提供了一系列扩展方法来简化Convention的添加不用自己再去转换:
通过代码调试发现,应用启动时遍历了系统中的所有控制器去执行Apply操作,那么通过IApplicationModelConvention一样也能实现这个功能,因为它里面包含了控制器集合:
publicclassApiControllerAuthorizeConvention:IApplicationModelConvention { publicvoidApply(ApplicationModelapplication) { foreach(varcontrollerinapplication.Controllers) { if(controller.Filters.Any(x=>xisApiControllerAttribute)&&!controller.Filters.Any(x=>xisAccessControlFilter)) { controller.Filters.Add(newAccessControlFilter()); } } } }
再改进一下
实际开发中我的AccessControlFilter需要通过构造函数注入业务接口,类似于这样:
publicclassAccessControlFilter:IActionFilter { privateIUserService_userService; publicAccessControlFilter(IUserServiceservice) { _userService=service; } publicvoidOnActionExecuting(ActionExecutingContextcontext) { //模拟一下业务操作 //varuser=_userService.GetById(996); //....... } publicvoidOnActionExecuted(ActionExecutedContextcontext) { } }
如何优雅的在Convention中使用DI自动注入呢?Asp.NetCoreMVC框架提供的ServiceFilter可以解决这个问题,ServiceFilter本身是一个过滤器,它的不同之处在于能够通过构造函数接收一个Type类型的参数,我们可以在这里把真正要用的过滤器传进去,于是上面的过滤器注册过程演变为:
controller.Filters.Add(newServiceFilterAttribute(typeof(AccessControlFilter)));
当然了,要从DI中获取这个filter实例,必须要把它注入到DI容器中:
services.AddScoped();
至此,大功告成,继续愉快的CRUD。
突然想起来我上篇文章提到的扩展DI属性注入功能估计也能通过这个玩意实现,eeeeeee...有空了试一下。
总结
总体来说,我通过曲线救国的方式实现了全局过滤器隔离,虽然去遍历目标控制器再手动添加Filter的方式没有那种一行代码就能实现的方式优雅,但我大体来说还算满意,是目前能想到的最好办法。我估摸着,options.Filters.Add(xxx)也是在框架某个时候一个个把xxx丢给各自主人的,瞎猜的,说错不负责~hhhh:see_no_evil::see_no_evil::see_no_evil:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。