ASP.NET Core中动态为控制器类型添加特性
Posted dotNET跨平台
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET Core中动态为控制器类型添加特性相关的知识,希望对你有一定的参考价值。
我在上一篇文章中介绍了如何为指定类型增加WebApi服务功能,达到了一定的解耦效果(不再需要继承Controller类,不再需要Controller后缀名),但是,Route
、HttpGet
之类的特性类型还是需要用Mvc的,那么今天这篇文章,就介绍如何把这些标签的依赖也消灭掉。
如果还没看上一篇文章又不了解如何指定类型为控制器的朋友,最好先看一下上一篇文章,因为要实现本文介绍的功能,是需要先实现上一篇文章所介绍的功能的。
实现原理
在没有MVC引用的类库中自定义RouteAttribute
、HttpGetAttribute
、HttpPostAttribute
等等跟MVC配对的特性标签类型;其次,在定义服务接口时把这些特性标记到方法声明上面;最后在ASP.NET Core 服务启动时自动按照指定的接口上的特性给相应的接口添加MVC的标签,使得动态指定的类型也能获得MVC的路由和请求方法限制的功能。
用到的技术点主要是反射和利用MVC框架给出的配置点。
MVC框架给出的配置点在IControllerModelConvention
、IActionModelConvention
、IParameterModelConvention
三个接口。顾名思义,它们分别对应控制器、操作、参数的类型配置,在它们的Apply
方法中,传入了一个MVC启动阶段扫描到的类型,开发者可以通过给这个类型添加各种MVC特性,如RouteData
、Attributes
、Filters
等。但是,这些特性组合是有规则的,不能一股脑儿地都添加进去,MVC的机制中会用SelectorModel
来将特性进行分组。
例如有以下的操作方法和它的特性:
[HttpGet]
[AcceptVerbs("POST", "PUT")]
[HttpPost("Api/Things")]
public void DoThing()
那么MVC中就需要把他们分为两组:
[HttpPost("Api/Things")]
[HttpGet], [AcceptVerbs("POST", "PUT")]
所以,这个分组的规则,我们怎么去处理呢?其实,在MVC的源码中就有这样的方法IList<SelectorModel> CreateSelectors(IList<object> attributes)
,我们把它复制过来就好了。
学以致用
回到我们的需求,我们需要把指定类型的方法都加上MVC特性,就需要自定义一些ModelConvention
。由于我们的需求比较简单,只需特性作用按类型来使指定类型获得MVC特性,我们只需在构造方法中传入指定类型,分别为控制器、操作和参数这三个作用类型都定义一个。由于篇幅问题,下面只贴Action的实现来讲解,另外两个大家可以看我项目中的源码。
using HttpGet = Microsoft.AspNetCore.Mvc.HttpGetAttribute;
using HttpPost = Microsoft.AspNetCore.Mvc.HttpPostAttribute;
using Route = Microsoft.AspNetCore.Mvc.RouteAttribute;
internal class ActionModelConvention : IActionModelConvention
{
//构造方法传入指定接口类型
public ActionModelConvention(Type serviceType)
{
this.serviceType = serviceType;
}
private Type serviceType { get; }
public void Apply(ActionModel action)
{
//判断是否为指定接口类型的实现类
if (!serviceType.IsAssignableFrom(action.Controller.ControllerType)) return;
var actionParams = action.ActionMethod.GetParameters();
//这串linq是查询出接口类型中与当前action相对应的方法,从中获取特性
var method = serviceType.GetMethods().FirstOrDefault(mth =>
{
var mthParams = mth.GetParameters();
return action.ActionMethod.Name == mth.Name
&& actionParams.Length == mthParams.Length
&& actionParams.Any(x => mthParams.Where(o => x.Name == o.Name).Any(o => x.GetType() == o.GetType()));
});
var attrs = method.GetCustomAttributes();
var actionAttrs = new List<object>();
foreach (var att in attrs)
{
//下面的HttpMethodAttribute是我们自己写的特性类型
if (att is HttpMethodAttribute methodAttr)
{
var httpMethod = methodAttr.Method;
var path = methodAttr.Path;
if (httpMethod == HttpMethod.Get)
{
//添加的HttpGet和HttpPost使用了命名空间别名
actionAttrs.Add(Activator.CreateInstance(typeof(HttpGet), path));
}
else if (httpMethod == HttpMethod.Post)
{
actionAttrs.Add(Activator.CreateInstance(typeof(HttpPost), path));
}
}
//下面的RouteAttribute是我们自己写的特性类型
if (att is RouteAttribute routeAttr)
{
actionAttrs.Add(Activator.CreateInstance(typeof(Route), routeAttr.Template));
}
}
if (actionAttrs.Any())
{
action.Selectors.Clear();
//AddRange静态方法就是从源码中复制过来的
ModelConventionHelper.AddRange(action.Selectors, ModelConventionHelper.CreateSelectors(actionAttrs));
}
}
}
上面代码其实还省略了其它的请求方式,我的源码中是有的,大家也可以前去查看。除了ActionModelConvention
,还需要写ControllerModelConvention
和ParameterModelConvention
。
配置到MVC框架
核心的代码写好了,那么怎么让它起作用呢?其实官方的源码已经提供了示例:Mvc/test/WebSites/ApplicationModelWebSite/Startup.cs,我们只需在services.AddMvc()
里的setupAction
委托中将我们的配置类型添加到MvcOptions.Conventions属性里就好了,这个属性是用来添加所有模型配置的,然后MVC启动后会把这些配置都扫描处理一遍。
来看看我这里的实现:
//假设我们定义了这样的接口
[Route("test")]
public interface ITestService
{
[Route("{name}"), HttpGet]
string Test(string name);
}
//AddMvc也一样,这里用AddMvcCore只是为了减少依赖
services.AddMvcCore(opt=>
{
opt.Conventions.Add(new ControllerModelConvention(typeof(ITestService)));
opt.Conventions.Add(new ActionModelConvention(typeof(ITestService)));
opt.Conventions.Add(new ParameterModelConvention(typeof(ITestService)));
})
当然了,我这里是因为指定类型是通过反射获取的,所以用了Type类型作为参数,大家也可以用泛型,在构造方法里获取对象类型。
完整代码实现
接下来,除了这三个ModelConvention
类型,我把整个实现代码贴一下,让大家看得比较直观,最后会跟上一篇文章的实现加入进来。因为,如果没有昨天的工作,我们指定的类型不被MVC识别为控制器的话,我们是无法实现为这些类型添加MVC属性的。
首先,先创建一个控制台程序,引入一下Nuget包
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.0.0" />
接着,定义一个接口以及它的实现,接口中标记了一些自定义特性,而实现类中完全没有:
[Route("test")]
public interface ITestService
{
[Route("{name}"), HttpGet]
string Test(string name);
}
public class TestService : ITestService
{
public string Test(string name)
{
return "Hello " + name;
}
}
然后在控制台程序的入口文件Program.cs的Main方法中写入一下代码:
internal class Program
{
public static void Main(string[] args)
{
new WebHostBuilder()
.UseKestrel()
.UseUrls("http://localhost:8080")
.ConfigureServices(services =>
{
//使用AddMvc亦可
services.AddMvcCore(opt=>
{
opt.Conventions.Add(new ControllerModelConvention(typeof(ITestService)));
opt.Conventions.Add(new ActionModelConvention(typeof(ITestService)));
opt.Conventions.Add(new ParameterModelConvention(typeof(ITestService)));
})
//下面这段是上一篇文章里的内容
.ConfigureApplicationPartManager(manager =>
{
var featureProvider = new ServiceControllerFeatureProvider(typeof(ITestService));
manager.FeatureProviders.Add(featureProvider);
});
})
.Configure(app => app.UseMvc())
.Build()
.Start();
}
}
一切编译通过后,点击运行,在浏览器中访问”http://localhost:8080/test/elderjames”,如果看到返回了“Hello elderjames”,那么就大功告成啦!
总结
这篇文章中主要介绍了通过实现IControllerModelConvention
、IActionModelConvention
、IParameterModelConvention
三个接口实现为指定为控制器的类型添加MVC特性的方法。
本篇文章发现源码的部分受到max zhang 和他的群里的群友福州 | Today的帮助,在此表示衷心的感谢。
在接下来的文章中,会介绍使用功能强大的.NTE Core开源AOP框架AspectCore实现的动态代理客户端,注册以上所说的接口,即可获得可以调用对应的WebApi服务的功能。这些工作的源码可以在我的框架示例项目中运行,大家有兴趣可以看看效果。
.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注
以上是关于ASP.NET Core中动态为控制器类型添加特性的主要内容,如果未能解决你的问题,请参考以下文章
将动态添加的 html 表行作为参数发布到控制器 - ASP.NET Core/MVC
分析路由时动态添加 Asp .NET Core Controller