ASP.NET Core中动态为控制器类型添加特性

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET Core中动态为控制器类型添加特性相关的知识,希望对你有一定的参考价值。

我在上一篇文章中介绍了如何为指定类型增加WebApi服务功能,达到了一定的解耦效果(不再需要继承Controller类,不再需要Controller后缀名),但是,RouteHttpGet之类的特性类型还是需要用Mvc的,那么今天这篇文章,就介绍如何把这些标签的依赖也消灭掉。

如果还没看上一篇文章又不了解如何指定类型为控制器的朋友,最好先看一下上一篇文章,因为要实现本文介绍的功能,是需要先实现上一篇文章所介绍的功能的。

实现原理

在没有MVC引用的类库中自定义RouteAttributeHttpGetAttributeHttpPostAttribute等等跟MVC配对的特性标签类型;其次,在定义服务接口时把这些特性标记到方法声明上面;最后在ASP.NET Core 服务启动时自动按照指定的接口上的特性给相应的接口添加MVC的标签,使得动态指定的类型也能获得MVC的路由和请求方法限制的功能。

用到的技术点主要是反射和利用MVC框架给出的配置点

MVC框架给出的配置点在IControllerModelConventionIActionModelConventionIParameterModelConvention三个接口。顾名思义,它们分别对应控制器、操作、参数的类型配置,在它们的Apply方法中,传入了一个MVC启动阶段扫描到的类型,开发者可以通过给这个类型添加各种MVC特性,如RouteDataAttributesFilters等。但是,这些特性组合是有规则的,不能一股脑儿地都添加进去,MVC的机制中会用SelectorModel来将特性进行分组。

例如有以下的操作方法和它的特性:

 
   
   
 
  1. [HttpGet]

  2. [AcceptVerbs("POST", "PUT")]

  3. [HttpPost("Api/Things")]

  4. public void DoThing()

那么MVC中就需要把他们分为两组:

  1. [HttpPost("Api/Things")]

  2. [HttpGet], [AcceptVerbs("POST", "PUT")]

所以,这个分组的规则,我们怎么去处理呢?其实,在MVC的源码中就有这样的方法IList<SelectorModel> CreateSelectors(IList<object> attributes),我们把它复制过来就好了。

学以致用

回到我们的需求,我们需要把指定类型的方法都加上MVC特性,就需要自定义一些ModelConvention。由于我们的需求比较简单,只需特性作用按类型来使指定类型获得MVC特性,我们只需在构造方法中传入指定类型,分别为控制器、操作和参数这三个作用类型都定义一个。由于篇幅问题,下面只贴Action的实现来讲解,另外两个大家可以看我项目中的源码。

 
   
   
 
  1. using HttpGet = Microsoft.AspNetCore.Mvc.HttpGetAttribute;

  2. using HttpPost = Microsoft.AspNetCore.Mvc.HttpPostAttribute;

  3. using Route = Microsoft.AspNetCore.Mvc.RouteAttribute;

  4. internal class ActionModelConvention : IActionModelConvention

  5. {

  6.    //构造方法传入指定接口类型

  7.    public ActionModelConvention(Type serviceType)

  8.    {

  9.        this.serviceType = serviceType;

  10.    }

  11.    private Type serviceType { get; }

  12.    public void Apply(ActionModel action)

  13.    {

  14.        //判断是否为指定接口类型的实现类

  15.        if (!serviceType.IsAssignableFrom(action.Controller.ControllerType)) return;

  16.        var actionParams = action.ActionMethod.GetParameters();

  17.        //这串linq是查询出接口类型中与当前action相对应的方法,从中获取特性

  18.        var method = serviceType.GetMethods().FirstOrDefault(mth =>

  19.        {

  20.            var mthParams = mth.GetParameters();

  21.            return action.ActionMethod.Name == mth.Name

  22.                   && actionParams.Length == mthParams.Length

  23.                   && actionParams.Any(x => mthParams.Where(o => x.Name == o.Name).Any(o => x.GetType() == o.GetType()));

  24.        });

  25.        var attrs = method.GetCustomAttributes();

  26.        var actionAttrs = new List<object>();

  27.        foreach (var att in attrs)

  28.            {

  29.                //下面的HttpMethodAttribute是我们自己写的特性类型

  30.                if (att is HttpMethodAttribute methodAttr)

  31.                {

  32.                    var httpMethod = methodAttr.Method;

  33.                    var path = methodAttr.Path;

  34.                    if (httpMethod == HttpMethod.Get)

  35.                    {

  36.                        //添加的HttpGet和HttpPost使用了命名空间别名

  37.                        actionAttrs.Add(Activator.CreateInstance(typeof(HttpGet), path));

  38.                    }

  39.                    else if (httpMethod == HttpMethod.Post)

  40.                    {

  41.                        actionAttrs.Add(Activator.CreateInstance(typeof(HttpPost), path));

  42.                    }

  43.                }

  44.                 //下面的RouteAttribute是我们自己写的特性类型

  45.                if (att is RouteAttribute routeAttr)

  46.                {

  47.                    actionAttrs.Add(Activator.CreateInstance(typeof(Route), routeAttr.Template));

  48.                }

  49.            }

  50.        if (actionAttrs.Any())

  51.        {

  52.            action.Selectors.Clear();

  53.            //AddRange静态方法就是从源码中复制过来的

  54.            ModelConventionHelper.AddRange(action.Selectors, ModelConventionHelper.CreateSelectors(actionAttrs));

  55.        }

  56.    }

  57. }

上面代码其实还省略了其它的请求方式,我的源码中是有的,大家也可以前去查看。除了ActionModelConvention,还需要写ControllerModelConventionParameterModelConvention

配置到MVC框架

核心的代码写好了,那么怎么让它起作用呢?其实官方的源码已经提供了示例:Mvc/test/WebSites/ApplicationModelWebSite/Startup.cs,我们只需在services.AddMvc()里的setupAction委托中将我们的配置类型添加到MvcOptions.Conventions属性里就好了,这个属性是用来添加所有模型配置的,然后MVC启动后会把这些配置都扫描处理一遍。

来看看我这里的实现:

 
   
   
 
  1. //假设我们定义了这样的接口

  2. [Route("test")]

  3. public interface ITestService

  4. {

  5.    [Route("{name}"), HttpGet]

  6.    string Test(string name);

  7. }

  8. //AddMvc也一样,这里用AddMvcCore只是为了减少依赖

  9. services.AddMvcCore(opt=>

  10.    {

  11.        opt.Conventions.Add(new ControllerModelConvention(typeof(ITestService)));

  12.        opt.Conventions.Add(new ActionModelConvention(typeof(ITestService)));

  13.        opt.Conventions.Add(new ParameterModelConvention(typeof(ITestService)));

  14.    })

当然了,我这里是因为指定类型是通过反射获取的,所以用了Type类型作为参数,大家也可以用泛型,在构造方法里获取对象类型。

完整代码实现

接下来,除了这三个ModelConvention类型,我把整个实现代码贴一下,让大家看得比较直观,最后会跟上一篇文章的实现加入进来。因为,如果没有昨天的工作,我们指定的类型不被MVC识别为控制器的话,我们是无法实现为这些类型添加MVC属性的。

首先,先创建一个控制台程序,引入一下Nuget包

 
   
   
 
  1.    <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.0.0" />

  2.    <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.0.0" />

接着,定义一个接口以及它的实现,接口中标记了一些自定义特性,而实现类中完全没有:

 
   
   
 
  1. [Route("test")]

  2. public interface ITestService

  3. {

  4.    [Route("{name}"), HttpGet]

  5.    string Test(string name);

  6. }

  7. public class TestService : ITestService

  8. {

  9.    public string Test(string name)

  10.    {

  11.        return "Hello " + name;

  12.    }

  13. }

然后在控制台程序的入口文件Program.cs的Main方法中写入一下代码:

 
   
   
 
  1. internal class Program

  2. {

  3.    public static void Main(string[] args)

  4.    {

  5.       new WebHostBuilder()

  6.            .UseKestrel()

  7.            .UseUrls("http://localhost:8080")

  8.            .ConfigureServices(services =>

  9.            {

  10.                //使用AddMvc亦可

  11.                services.AddMvcCore(opt=>

  12.                {

  13.                    opt.Conventions.Add(new ControllerModelConvention(typeof(ITestService)));

  14.                    opt.Conventions.Add(new ActionModelConvention(typeof(ITestService)));

  15.                    opt.Conventions.Add(new ParameterModelConvention(typeof(ITestService)));

  16.                })

  17.                //下面这段是上一篇文章里的内容

  18.                .ConfigureApplicationPartManager(manager =>

  19.                {

  20.                    var featureProvider = new ServiceControllerFeatureProvider(typeof(ITestService));

  21.                    manager.FeatureProviders.Add(featureProvider);

  22.                });

  23.            })

  24.            .Configure(app => app.UseMvc())

  25.            .Build()

  26.            .Start();

  27.    }

  28. }

一切编译通过后,点击运行,在浏览器中访问”http://localhost:8080/test/elderjames”,如果看到返回了“Hello elderjames”,那么就大功告成啦!

总结

这篇文章中主要介绍了通过实现IControllerModelConventionIActionModelConventionIParameterModelConvention三个接口实现为指定为控制器的类型添加MVC特性的方法。

本篇文章发现源码的部分受到max zhang 和他的群里的群友福州 | Today的帮助,在此表示衷心的感谢。

在接下来的文章中,会介绍使用功能强大的.NTE Core开源AOP框架AspectCore实现的动态代理客户端,注册以上所说的接口,即可获得可以调用对应的WebApi服务的功能。这些工作的源码可以在我的框架示例项目中运行,大家有兴趣可以看看效果。


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

以上是关于ASP.NET Core中动态为控制器类型添加特性的主要内容,如果未能解决你的问题,请参考以下文章

将动态添加的 html 表行作为参数发布到控制器 - ASP.NET Core/MVC

分析路由时动态添加 Asp .NET Core Controller

ASP.NET Core MVC 中两种路由的简单配置

[Asp.Net Core]FilterFactory扩展定制

角色动态授权 asp.net core

ASP.Net Core MVC 如何针对强类型模型发布未知数量的字段