ASP.NET Core MVC应用模型的构建[4]: Action的选择
ControllerModel类型的Actions属性包含一组描述有效Action方法的ActionModel对象。对于定义在Controller类型中的所有方法,究竟哪些方法才能成为有效的Action方法呢?所以在正式介绍ActionModel类型之前,我们先来聊聊Action方法的选择规则。
一、Action方法的选择
二、ActionModel
三、PropertyModel与ParameterModel
四、SelectorModel
五、实例演示:选择器模型的构建
一、 Action方法的选择
当DefaultApplicationModelProvider对象根据某个具体的Controller类型创建对应ControllerModel对象的时候,它会提取出定义在该类型中的方法,并按照预定义的规则选择出有效的Action方法。有效的Action方法必须满足如下条件:
- 公共方法:私有(Private)、内部(Internal)和受保护(Protected)方法均为无效Action方法。
- 非抽象方法:抽象方法为无效的Action方法(这个限制可以忽略,因为Controller不会是抽象类型)。
- 非泛型方法:Action方法不能定义成泛型方法。
- 非静态方法:静态方法为无效Action方法。
- 不是从Object类型上继承的方法:Action方法支持继承,但是从Object类型上继承的方法不能成为Action方法。
- 不是对IDisposable接口的实现:如果Controller类型实现了IDisposable接口,实现的Dispose方法不是有效的Action方法。
二、ActionModel
如下面的代码片段所示,ActionModel类型实现了ICommonModel、IFilterModel和IApiExplorerModel三个接口。默认注册的DefaultApplicationModelProvider会对ActionModel对象做如下的设置:MemberInfo和ActionMethod属性都将设置为描述当前Action方法的MethodInfo对象。通过标注的特性注册到Action方法上的过滤器会被提取出来,对应的元数据会添加到Filters属性中。ApiExplorer属性返回的ApiExplorerModel对象由标注在Action方法上实现了IApiDescriptionGroupNameProvider和IApiDescriptionVisibilityProvider接口的特性构建而成。
public class ActionModel : ICommonModel, IFilterModel, IApiExplorerModel { public ControllerModel Controller { get; set; } public IList<IFilterMetadata> Filters { get; } public ApiExplorerModel ApiExplorer { get; set; } public IDictionary<object, object> Properties { get; } public IList<ParameterModel> Parameters { get; } public IList<SelectorModel> Selectors { get; } public MethodInfo ActionMethod { get; } public string DisplayName { get; } public string ActionName { get; set; } public IReadOnlyList<object> Attributes { get; } public IOutboundParameterTransformer RouteParameterTransformer { get; set; } public IDictionary<string, string> RouteValues { get; } MemberInfo ICommonModel.MemberInfo { get; } string ICommonModel.Name { get; } }
DefaultApplicationModelProvider会为Action方法的每个参数创建一个ParameterModel对象,并添加到Parameters属性中。应用在Action方法上的用于封装路由信息(特性路由、约束和终结点元数据)的SelectorModel对象会按照上述的方式构建出来,并添加到Selectors属性中。标注在Action方法上的特性会被提取出来并添加到Attributes属性返回的列表中。表示Action名称的ActionName与Name属性具有相同的值,DefaultApplicationModelProvider会默认将它们设置为方法的名称,但是我们可以在方法上通过标注如下这个ActionNameAttribute特性对该属性进行设置。
[AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)] public sealed class ActionNameAttribute : Attribute { public string Name { get; } public ActionNameAttribute(string name); }
我们照例通过一个简单的示例来演示一下DefaultApplicationModelProvider对象针对ActionModel的构建规则。我们依然沿用前面创建的演示实例,这次我们将FoobarController定义成如下的形式。如代码片段所示,我们为Action方法定义了三个参数,并在上面标注了四个特性,其中FoobarAttribute特性是我们应用在该Action方法上的过滤器,ApiExplorerSettingsAttribute特性针对ApiExplorer作了相应设置,HttpGetAttribute特性完成了针对路由和HTTP方法约束的定义,ActionNameAttribute特性则将Action名称设置为“Baz”。
public class FoobarController { [Foobar] [ApiExplorerSettings(GroupName ="test")] [HttpGet("/foobar/baz")] [ActionName("Baz")] public void Index(string foo, string bar, string baz) => throw new NotImplementedException(); }
我们需要修改定义在HomeController中的Action方法Index。如下面的代码片段所示,在利用通过参数注入的ApplicationModelProducer对象根据提供的FoobarController类型创建出对应ApplicationModel对象之后,我们提取出描述Action方法Index的ActionModel对象,并将其作为Model呈现在默认的View中。
public class HomeController : Controller { [HttpGet("/")] public IActionResult Index([FromServices]ApplicationModelProducer producer) { var applicationModel = producer.Create(typeof(FoobarController)); return View(applicationModel.Controllers.Single().Actions.Single()); } }
我们将按照如下的形式重新定义了Action方法Index对应的View。如下面的代码片段所示,这是一个Model类型为ActionModel的强类型View,,它将ActionModel承载的元数据呈现在一个表格中。
@model Microsoft.AspNetCore.Mvc.ApplicationModels.ActionModel @{ var filters = Model.Filters; var routeValues = Model.RouteValues.ToArray(); var parameters = Model.Parameters; var attributes = Model.Attributes; } <html> <head> <title>Action</title> </head> <body> <table border="1" cellpadding="0" cellspacing="0"> <tr><td>Method</td><td>@Model.ActionMethod.Name</td></tr> <tr><td>ActionName</td><td>@Model.ActionName</td></tr> <tr><td>DisplayName</td><td>@Model.DisplayName</td></tr> <tr> <td rowspan="@parameters.Count">Parameters</td> <td>@parameters[0].Name</td> </tr> @for (int index = 1; index < parameters.Count; index++) { <tr><td>@parameters[index].Name</td></tr> } <tr> <td rowspan="@filters.Count">Filters</td> <td>@filters[0].GetType().Name</td> </tr> @for (int index = 1; index < filters.Count; index++) { <tr><td>@filters[index].GetType().Name</td></tr> } <tr> <td rowspan="@attributes.Count">Attributes</td> <td>@attributes[0].GetType().Name</td> </tr> @for (int index = 1; index < attributes.Count; index++) { <tr><td>@attributes[index].GetType().Name</td></tr> } @if (routeValues.Length == 0) { <tr><td>RouteValues</td><td>N/A</td></tr> } else { <tr> <td rowspan="@routeValues.Length">RouteValues</td> <td>@routeValues[0].Key = @routeValues[0].Value</td> </tr> } @for (int index = 1; index < routeValues.Length; index++) { <tr><td>@routeValues[index].Key = @routeValues[index].Value</td></tr> } <tr> <td rowspan="2">ApiExplorer</td> <td>IsVisible = @Model.ApiExplorer.IsVisible </td> </tr> <tr> <td>GroupName = @Model.ApiExplorer.GroupName </td> </tr> </table> </body> </html>
改动后的演示程序启动后,我们利用浏览器访问应用的主页,可以得到如图1所示的输出结果。我们从图中可以看出,Action名称来源于标注在方法上的ActionNameAttribute特性。DefaultApplicationModelProvider会为方法的每个参数创建一个ParameterModel对象并添加到ActionModel对象的Properties属性中。通过特性标注注册到Action方法上的FoobarAttribute过滤器被添加到ActionModel对象的Filters属性中。Action方法标注的四个特性全部被添加到ActionModel对象的Attributes属性中。ActionModel对象的ApiExplorer属性返回的ApiExplorerModel对象是由标注在方法上的ApiExplorerSettingsAttribute特性构建的。值得一提的是,Controller和Action的名称此时并没有作为路由参数添加到RouteValues属性中。
图1 Action模型默认的构建规则
三、PropertyModel与ParameterModel
默认注册的DefaultApplicationModelProvider会将定义在Controller类型的公共属性(包括从基类继承的属性)提取创建,然后创建相应的PropertyModel对象并添加到ControllerModel对象的ControllerProperties属性中。描述属性的PropertyModel对象和描述参数的ParameterModel对象都是为了提供模型绑定的元数据,所以它们具有相同的基类ParameterModelBase。
public abstract class ParameterModelBase : IBindingModel { public string Name { get; protected set; } public Type ParameterType { get; } public IReadOnlyList<object> Attributes { get; } public BindingInfo BindingInfo { get; set; } public IDictionary<object, object> Properties { get; } }
如上面的代码片段所示,抽象类ParameterModelBase实现了IBindingModel,所以它需要利用实现的BindingInfo书信提供模型绑定信息。ParameterModelBase并没有实现IPropertyModel接口,但是其自身提供了一个Properties属性。ParameterModelBase的Name和ParameterType属性分别表示对应参数/属性的名称和类型。标注到属性上的特性会添加到Attributes属性中。如下所示的是描述Controller属性的PropertyModel类型,和描述Action方法参数的ParameterModel类型的定义。
public class PropertyModel : ParameterModelBase, ICommonModel, IBindingModel { public ControllerModel Controller { get; set; } public PropertyInfo PropertyInfo { get; } public string PropertyName { get; set; } public IReadOnlyList<object> Attributes { get; } public IDictionary<object, object> Properties { get; } MemberInfo ICommonModel.MemberInfo { get; } } public class ParameterModel : ParameterModelBase, ICommonModel { public ActionModel Action { get; set; } public ParameterInfo ParameterInfo { get; } public string ParameterName { get; set; } public string DisplayName { get; } public IReadOnlyList<object> Attributes { get; } public IDictionary<object, object> Properties { get; } MemberInfo ICommonModel.MemberInfo { get; } }
四、SelectorModel
SelectorModel类型是对Action选择器(Selector)的描述,这里的选择器旨在解决如何为请求选择匹配Action的问题,所以它承载的其实针对路由的原始定义。如下面的代码片段所示,SelectorModel类型通过AttributeRouteModel、ActionConstraints和EndpointMetadata分别存储了特性路由信息、约束和终结点元数据。
public class SelectorModel { public AttributeRouteModel AttributeRouteModel { get; set; } public IList<IActionConstraintMetadata> ActionConstraints { get; } public IList<object> EndpointMetadata { get; } } public class AttributeRouteModel { public IRouteTemplateProvider Attribute { get; } public string Template { get; set; } public int? Order { get; set; } public string Name { get; set; } public bool SuppressLinkGeneration { get; set; } public bool SuppressPathMatching { get; set; } public bool IsAbsoluteTemplate { get; } }
由于路由可以通过标注到Controller类型或者Action方法上相应的特性来定义,所以描述Controller类型和Action方法的ControllerModel和ActionModel类型都具有一个Selectors属性来保存各自的选择器,DefaultApplicationModelProvider针对它们的解析方式也是一致的。这里用来定义路由的特性实现了如下所示的IRouteTemplateProvider特性。
public interface IRouteTemplateProvider { string Template { get; } int? Order { get; } string Name { get; } }
顾名思义,实现IRouteTemplateProvider接口的特性旨在定义一个针对指定模板的路由。除此之外,针对SelectorModel的构建还涉及另一个名为IActionHttpMethodProvider的接口,实现该接口的特性为目标Action定义针对HTTP方法的约束。
public interface IActionHttpMethodProvider { IEnumerable<string> HttpMethods { get; } }
对于目前提供的实现了IActionHttpMethodProvider接口的特性来说,它们无一例外都同时实现了IRouteTemplateProvider接口。比如下面这个AcceptVerbsAttribute特性可以标注到Action方法上指定一组支持的HTTP方法,同时也可以利用三个属性(Route、Name和Order)对路由作相应的定义(显式实现的Template和Order属性与自身Route和Order属性具有相同的值)。顺便提一下,我们在AcceptVerbsAttribute特性构造函数以字符串指定的HTTP方法名称会一律转换成大写形式。
[AttributeUsage( AttributeTargets.Method, AllowMultiple=true, Inherited=true)] public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider { public IEnumerable<string> HttpMethods { get; } public string Route { get; set; } public int Order { get; set; } public string Name { get; set; } string IRouteTemplateProvider.Template { get; } int? IRouteTemplateProvider.Order { get; } public AcceptVerbsAttribute(string method); public AcceptVerbsAttribute(params string[] methods); }
虽然AcceptVerbsAttribute特性可以为我们指定多个支持的HTTP方法,但是我们似乎更倾向于使用针对具有某种HTTP方法的特性,比如HttpGetAttribute、HttpPostAttribute、HttpPutAttribute、HttpDeleteAttribute、HttpHeadAttribute、HttpPatchAttribute和HttpOptionsAttribute特性,它们都派生于如下这个抽象的HttpMethodAttribute特性类型。
[AttributeUsage( AttributeTargets.Method, AllowMultiple=true, Inherited=true)] public abstract class HttpMethodAttribute : Attribute, IActionHttpMethodProvider, IRouteTemplateProvider { public IEnumerable<string> HttpMethods { get; } public string Template { get; } public int Order { get; set; } public string Name { get; set; } int? IRouteTemplateProvider.Order { get; } public HttpMethodAttribute(IEnumerable<string> httpMethods); public HttpMethodAttribute(IEnumerable<string> httpMethods, string template); }
IRouteTemplateProvider接口除了上述这些实现类型之外,如下这个专门用来定义路由的RouteAttribute特性单独实现了该接口。从提供的代码片段可以看出,RouteAttribute特性可以标注在Controller类型或者具体的Action方法上。
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple=true, Inherited=true)] public class RouteAttribute : Attribute, IRouteTemplateProvider { public string Template { get; } public int Order { get; set; } public string Name { get; set; } public RouteAttribute(string template); int? IRouteTemplateProvider.Order { get; } }
DefaultApplicationModelProvider针对SelectorModel对象的构建逻辑分为两种情况。如果标注在当前Controller类型或者Action方法上的特性没有提供任何路由信息,这种情况由分为两种场景:其一,根本没有标注任何实现了IRouteTemplateProvider接口的特性;其二,所有的特性并没有对定义在该接口的三个属性(Template、Name和Order)做任何设置,比如我们经常在Action方法上标注一个没有提供任何参数的HttpGetAttribute特性,其目的只是限制支持的HTTP方法,而不是定义路由。
在这种情况下,DefaultApplicationModelProvider会创建一个唯一的SelectorModel对象。由于没有任何路由被定义,所以该对象的AttributeRouteModel属性会返回Null。标注在Controller类型或者Action方法上实现了IActionConstraintMetadata接口的特性会作为约束添加到ActionConstraints属性中。如果标注了实现IActionHttpMethodProvider接口的特性对HTTP方法做了限制,一个对应的HttpMethodActionConstraint对象会额外创建出来并添加到ActionConstraints属性中。与此同时,一个针对HTTP方法列表的HttpMethodMetada对象会被创建出来并作为终结点元数据被添加到EndpointMetadata属性中。除此之外,所有特性都会作为终结点元数据添加到EndpointMetadata属性中。
如果当前Controller类型或者Action方法上标注了实现IRouteTemplateProvider接口的特性,并且作了具体的路由设置,DefaultApplicationModelProvider会为每个IRouteTemplateProvider对象创建一个SelectorModel对象。对于每一个针对IRouteTemplateProvider对象创建的SelectorModel对象来说,设置的路由信息会被提取出来用于创建对应的AttributeRouteModel对象。如果当前特性是一个IActionHttpMethodProvider对象,一个对应的HttpMethodActionConstraint对象会额外创建出来并添加到ActionConstraints属性中。与此同时一个针对HTTP方法列表的HttpMethodMetada对象会被创建出来,当前特性和这个对象都将作为终结点元数据被添加到EndpointMetadata属性中。
如果当前IRouteTemplateProvider对象类型为RouteAttribute,那些没有提供路由信息的实现了IActionHttpMethodProvider接口的特性(即只定义了HTTP方法约束的特性)会被提取出来,一个根据它们提供的HTTP方法列表创建的HttpMethodActionConstraint对象并添加到ActionConstraints属性中。与此同时,一个针对HTTP方法列表的HttpMethodMetada对象会被创建出来并作为终结点元数据被添加到EndpointMetadata属性中。EndpointMetadata属性最终包含的终结点元数据还包括当前RouteAttribute特性和这些单纯定义约束的特性。
五、实例演示:选择器模型的构建
对于DefaultApplicationModelProvider为Controller类型或者Action方法构建SelectorModel的逻辑,我想针对具体的场景会更好理解一点,为此我们来演示几个简单的实例。我们依然沿用前面的演示程序,并将FoobarController类型改写成如下的形式。FoobarController类型中定义了三个Action方法,接下来我们看看DefaultApplicationModelProvider对象会为它们创建出怎样的选择器。
public class FoobarController { [HttpGet] [HttpPost] public void Foo()=>throw new NotImplementedException(); [HttpGet("bar")] [HttpPost("bar")] public void Bar() => throw new NotImplementedException(); [HttpGet()] [HttpPost("bar")] [Route("bar")] public void Baz() => throw new NotImplementedException(); }
我们对定义在HomeController中的Action方法Index作如下的修改。如代码片段所示,在该方法中,我们利用通过参数注入的ApplicationModelProducer对象根据FoobarController类型创建出对应的ApplicationModel对象,然后利用查询字符串绑定的actionName参数提取出描述对应Action的ActionModel对象。我们将ActionModel对象的Selectors属性提取的选择器列表作为Model呈现在View中。
public class HomeController : Controller { [HttpGet("/")] public IActionResult Index([FromServices]ApplicationModelProducer producer, [FromQuery]string actionName) { var applicationModel = producer.Create(typeof(FoobarController)); var actionModel = applicationModel.Controllers.Single().Actions.Single(it=>string.Compare(it.ActionName, actionName, true) == 0); return View(actionModel.Selectors); } }
如下所示的是修改后的View。如代码片段所示,这个是一个Model类型为IList<SelectorModel>的强类型View,我们将提供的用于描述选择器的每个SelectorModel对象的元数据信息呈现在表格中。
@using Microsoft.AspNetCore.Mvc.ApplicationModels; @model IList<SelectorModel> <html> <head> <title>Selectors</title> </head> <body> <table border="1" cellpadding="0" cellspacing="0"> @for (int i = 0; i < Model.Count; i++) { var selector = Model[i]; var constraints = selector.ActionConstraints; var metadata = selector.EndpointMetadata; <tr><td colspan="2">Selector @(i+1)</td></tr> <tr> <td>AttributeRouteModel</td> <td>@selector.AttributeRouteModel?.Template</td> </tr> <tr> <td rowspan="@constraints.Count">ActionConstraints</td> <td>@constraints[0].GetType().Name</td> </tr> @for (int j = 1; j < constraints.Count; j++) { <tr><td>@constraints[j].GetType().Name</td></tr> } <tr> <td rowspan="@metadata.Count">EndpointMetadata</td> <td>@metadata[0].GetType().Name</td> </tr> @for (int j = 1; j < metadata.Count; j++) { <tr><td>@metadata[j].GetType().Name</td></tr> } } </table> </body> </html>
由于第一个Action方法Foo上的两个IActionHttpMethodProvider特性并没有提供任何的路由信息,所以它只具有一个AttributeRouteModel属性为Null的SelectorModel对象。这两个特性提供的针对HTTP方法(GET和POST)的约束会转换成一个HttpMethodActionConstraint对象并添加到SelectorModel对象的ActionConstraints属性中。除此之外,这两个特性会直接作为终结点元数据被添加到SelectorModel对象的EndpointMetadata属性中,该属性还会包含一个针对HTTP方法约束的HttpMethodMetada对象。图2所示的就是演示应用返回的针对Action方法Foo的选择器信息。
图2 Action方法Foo的选择器
第二个Action方法Bar上的两个特性均指定了路由模板,所以DefaultApplicationModelProvider会为它创建两个针对性的SelectorModel对象。DefaultApplicationModelProvider会根据特性(HttpGetAttribute和HttpPostAttribute)提供的路由信息来创建对应的AttributeRouteModel对象。SelectorModel对象ActionConstraints属性会包含根据各自提供的HTTP方法约束创建的HttpMethodActionConstraint对象。EndpointMetadata属性将会包含两个终结点元数据对象,分别是当前的特性和根据HTTP方法约束创建的HttpMethodMetada对象。图3所示的就是演示应用返回的针对Action方法Bar的选择器列表。
图3 Action方法Bar的选择器
第三个Action方法方法上标注了三个特性,但是其中只有两个特性提供了路由信息,所以DefaultApplicationModelProvider最终会根据标注的HttpPostAttribute和RouteAttribute特性创建出两个对应的SelectorModel对象。根据标注的HttpPostAttribute特性针对SelectorModel对象的创建与上面一致,所以我们现在只关注针对RouteAttribute特性创建的SelectorModel对象。该对象提供的AttributeRouteModel对象自然由RouteAttribute特性提供的路由信息来创建。
该方法上没有提供路由信息的HttpGetAttribute特性将被用来提供当前路由的约束,所以这个SelectorModel对象的ActionConstraints属性中会包含一个根据这个特性创建的HttpMethodActionConstraint对象。这个SelectorModel对象的EndpointMetadata属性中最终会包含三个终结点元数据,分别是标注的RouteAttribute和HttpGetAttribute特性,以及根据HTTP方法约束创建的HttpMethodMetada对象。图4所示的就是演示应用返回的针对Action方法Baz的选择器列表。
图4 Action方法Bar的选择器
ASP.NET Core MVC应用模型的构建[1]: 应用的蓝图
ASP.NET Core MVC应用模型的构建[2]: 应用模型
ASP.NET Core MVC应用模型的构建[3]: Controller模型
ASP.NET Core MVC应用模型的构建[4]: Action模型