这来自于我把项目迁移到Asp.Net Core的过程当中碰到一个问题。在一个web程序中同时包含了MVC和WebAPI,如今须要给WebAPI部分单独添加一个接口验证过滤器IActionFilter
,常规作法通常是写好过滤器后给须要的控制器挂上这个标签,高级点的作法是注册一个全局过滤器,这样能够避免每次手动添加同时代码也更好管理。注册全局过滤器的方式为:html
services.AddMvc(options => { options.Filters.Add(typeof(AccessControlFilter)); });
但这样作会带来一个问题,那就是MVC部分控制器也会受影响,虽然能够在过滤器中进行一些判断来区分哪些是MVC Controller哪些是API Controller,可是无缘无故给MVC增长这么一个没用的Filter,反正我是不能忍,因此寻找有没有更好的办法来实现这个功能。
因而ModelConvention(能够翻译为模型约定)闪亮登场。web
看一下官方文档是怎么描述应用程序模型(ApplicationModel)的:c#
ASP.NET Core MVC defines an application model representing the components of an MVC app. You can read and manipulate this model to modify how MVC elements behave. By default, MVC follows certain conventions to determine which classes are considered to be controllers, which methods on those classes are actions, and how parameters and routing behave. You can customize this behavior to suit your app's needs by creating your own conventions and applying them globally or as attributes.markdown
简单一点说,ApplicationModel描述了MVC应用中的各类对象和行为,这些内容包含Application、Controller、Action、Parameter、Router、Page、Property、Filter等等,而Asp.Net Core框架自己内置一套规则(Convention)用来处理这些模型,同时也提供了接口给咱们自定义约定来扩展模型以实现更符合须要的应用。app
和应用程序模型有关的类都定义在命名空间Microsoft.AspNetCore.Mvc.ApplicationModels
中,这些模型经过IApplicationModelProvider
构建出来,Asp.Net Core框架提供的默认Provider是DefaultApplicationModelProvider
。咱们能够编辑这些模型,从而更改它的表现行为,这就要借助它的ModelConvention来实现。框架
ModelConvention定义了操做模型的入口,又或者说是一种契约,经过它咱们能够对模型进行修改,经常使用的Convention包括:ide
这些接口提供了一个共同的方法Apply
,方法参数是各自的应用程序模型,以IControllerModelConvention
为例看一下它的定义:函数
namespace Microsoft.AspNetCore.Mvc.ApplicationModels { // // 摘要: // Allows customization of the Microsoft.AspNetCore.Mvc.ApplicationModels.ControllerModel. // // 言论: // To use this interface, create an System.Attribute class which implements the // interface and place it on a controller class. Microsoft.AspNetCore.Mvc.ApplicationModels.IControllerModelConvention // customizations run after Microsoft.AspNetCore.Mvc.ApplicationModels.IApplicationModelConvention // customizations and before Microsoft.AspNetCore.Mvc.ApplicationModels.IActionModelConvention // customizations. public interface IControllerModelConvention { // // 摘要: // Called to apply the convention to the Microsoft.AspNetCore.Mvc.ApplicationModels.ControllerModel. // // 参数: // controller: // The Microsoft.AspNetCore.Mvc.ApplicationModels.ControllerModel. void Apply(ControllerModel controller); } }
从接口摘要能够看到,这个接口容许自定义ControllerModel
对象,而如何自定义内容正是经过Apply
方法来实现,这个方法提供了当前ControllerModel
对象的实例,咱们能够在它身上获取到的东西实在太多了,看看它包含些什么:
ui
有了这些,咱们能够作不少很灵活的操做,例如经过设置ControllerName
字段强制更改控制器的名称让程序中写死的控制器名失效,也能够经过Filters
字段动态更新它的过滤器集合,经过RouteValues
来更改路由规则等等。this
说到这里,不少人会以为这玩意儿和自定义过滤器看起来差很少,最开始我也这么认为,但通过实际代码调试我发现它的生命周期要比过滤器早的多,或者说根本没法比较,这个家伙只须要在应用启动时执行一次并不用随着每次请求而执行。也就是说,它的执行时间比激活控制器还要早,那时候根本没有过滤器什么事儿,它的调用是发生在app.UseEndpoints()
。
回到最开始的需求。基于上面的介绍,咱们能够自定义以下的约定:
public class ApiControllerAuthorizeConvention : IControllerModelConvention { public void Apply(ControllerModel controller) { if (controller.Filters.Any(x => x is ApiControllerAttribute) && !controller.Filters.Any(x => x is AccessControlFilter)) { controller.Filters.Add(new AccessControlAttribute()); } } }
上面的主要思路就是经过判断控制器自己的过滤器集合是否包含ApiControllerAttribute
来识别是否API Controller,若是是API Controller而且没有标记过AccessControlAttribute
的话就新建一个实例加入进去。
那么如何把这个约定注册到应用中呢?在Microsoft.AspNetCore.Mvc.MvcOptions中提供了Conventions
属性:
// // 摘要: // Gets a list of Microsoft.AspNetCore.Mvc.ApplicationModels.IApplicationModelConvention // instances that will be applied to the Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModel // when discovering actions. public IList<IApplicationModelConvention> Conventions { get; }
经过操做它就能把自定义约定注入进去:
services.AddMvc(options => { options.Conventions.Add(new ApiControllerAuthorizeConvention()); })
细心的人会发现,Conventions是一个IApplicationModelConvention
类型的集合,而咱们自定义的Convention是一个IControllerModelConvention
,正常来讲应该会报错才对?缘由是Asp.Net Core的DI框架帮咱们提供了一系列扩展方法来简化Convention的添加不用本身再去转换:
经过代码调试发现,应用启动时遍历了系统中的全部控制器去执行Apply操做,那么经过IApplicationModelConvention
同样也能实现这个功能,由于它里面包含了控制器集合:
public class ApiControllerAuthorizeConvention : IApplicationModelConvention { public void Apply(ApplicationModel application) { foreach (var controller in application.Controllers) { if (controller.Filters.Any(x => x is ApiControllerAttribute) && !controller.Filters.Any(x => x is AccessControlFilter)) { controller.Filters.Add(new AccessControlFilter()); } } } }
实际开发中个人AccessControlFilter须要经过构造函数注入业务接口,相似于这样:
public class AccessControlFilter : IActionFilter { private IUserService _userService; public AccessControlFilter(IUserService service) { _userService = service; } public void OnActionExecuting(ActionExecutingContext context) { //模拟一下业务操做 //var user=_userService.GetById(996); //....... } public void OnActionExecuted(ActionExecutedContext context) { } }
如何优雅的在Convention中使用DI自动注入呢?Asp.Net Core MVC框架提供的ServiceFilter
能够解决这个问题,ServiceFilter
自己是一个过滤器,它的不一样之处在于可以经过构造函数接收一个Type类型的参数,咱们能够在这里把真正要用的过滤器传进去,因而上面的过滤器注册过程演变为:
controller.Filters.Add(new ServiceFilterAttribute(typeof(AccessControlFilter)));
固然了,要从DI中获取这个filter实例,必需要把它注入到DI容器中:
services.AddScoped<AccessControlFilter>();
至此,大功告成,继续愉快的CRUD。
忽然想起来我上篇文章提到的扩展DI属性注入功能估计也能经过这个玩意实现,eeeeeee...有空了试一下。
整体来讲,我经过曲线救国的方式实现了全局过滤器隔离,虽然去遍历目标控制器再手动添加Filter的方式没有那种一行代码就能实现的方式优雅,但我大致来讲还算满意,是目前能想到的最好办法。我估摸着,options.Filters.Add(xxx)
也是在框架某个时候一个个把xxx丢给各自主人的,瞎猜的,说错不负责~hhhh🙈🙈🙈
第一次用markdown写博客真香