前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ASP.NET Core MVC应用模型的构建[4]: Action的选择

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

作者头像
蒋金楠
发布2024-03-01 14:12:21
850
发布2024-03-01 14:12:21
举报
文章被收录于专栏:大内老A大内老A

ControllerModel类型的Actions属性包含一组描述有效Action方法的ActionModel对象。对于定义在Controller类型中的所有方法,究竟哪些方法才能成为有效的Action方法呢?所以在正式介绍ActionModel类型之前,我们先来聊聊Action方法的选择规则。

一、 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接口的特性构建而成。

代码语言:javascript
复制
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特性对该属性进行设置。

代码语言:javascript
复制
[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”。

代码语言:javascript
复制
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中。

代码语言:javascript
复制
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承载的元数据呈现在一个表格中。

代码语言:javascript
复制
@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属性中。

clip_image002
clip_image002

图1 Action模型默认的构建规则

三、PropertyModel与ParameterModel

默认注册的DefaultApplicationModelProvider会将定义在Controller类型的公共属性(包括从基类继承的属性)提取创建,然后创建相应的PropertyModel对象并添加到ControllerModel对象的ControllerProperties属性中。描述属性的PropertyModel对象和描述参数的ParameterModel对象都是为了提供模型绑定的元数据,所以它们具有相同的基类ParameterModelBase。

代码语言:javascript
复制
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类型的定义。

代码语言:javascript
复制
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分别存储了特性路由信息、约束和终结点元数据。

代码语言:javascript
复制
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特性。

代码语言:javascript
复制
public interface IRouteTemplateProvider
{
    string 	Template { get; }
    int? 	Order { get; }
    string 	Name { get; }
}

顾名思义,实现IRouteTemplateProvider接口的特性旨在定义一个针对指定模板的路由。除此之外,针对SelectorModel的构建还涉及另一个名为IActionHttpMethodProvider的接口,实现该接口的特性为目标Action定义针对HTTP方法的约束。

代码语言:javascript
复制
public interface IActionHttpMethodProvider
{
    IEnumerable<string> HttpMethods { get; }
}

对于目前提供的实现了IActionHttpMethodProvider接口的特性来说,它们无一例外都同时实现了IRouteTemplateProvider接口。比如下面这个AcceptVerbsAttribute特性可以标注到Action方法上指定一组支持的HTTP方法,同时也可以利用三个属性(Route、Name和Order)对路由作相应的定义(显式实现的Template和Order属性与自身Route和Order属性具有相同的值)。顺便提一下,我们在AcceptVerbsAttribute特性构造函数以字符串指定的HTTP方法名称会一律转换成大写形式。

代码语言:javascript
复制
[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特性类型。

代码语言:javascript
复制
[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方法上。

代码语言:javascript
复制
[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对象会为它们创建出怎样的选择器。

代码语言:javascript
复制
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中。

代码语言:javascript
复制
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对象的元数据信息呈现在表格中。

代码语言:javascript
复制
@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的选择器信息。

clip_image004
clip_image004

图2 Action方法Foo的选择器

第二个Action方法Bar上的两个特性均指定了路由模板,所以DefaultApplicationModelProvider会为它创建两个针对性的SelectorModel对象。DefaultApplicationModelProvider会根据特性(HttpGetAttribute和HttpPostAttribute)提供的路由信息来创建对应的AttributeRouteModel对象。SelectorModel对象ActionConstraints属性会包含根据各自提供的HTTP方法约束创建的HttpMethodActionConstraint对象。EndpointMetadata属性将会包含两个终结点元数据对象,分别是当前的特性和根据HTTP方法约束创建的HttpMethodMetada对象。图3所示的就是演示应用返回的针对Action方法Bar的选择器列表。

clip_image006
clip_image006

图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的选择器列表。

clip_image008
clip_image008

图4 Action方法Bar的选择器

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-02-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、 Action方法的选择
  • 二、ActionModel
  • 三、PropertyModel与ParameterModel
  • 四、SelectorModel
  • 五、实例演示:选择器模型的构建
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档