ASP.NET Core MVC应用模型的构建[4]: Action的选择

时间:2024-02-29 14:06:07

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模型