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屬性中。

clip_image002

圖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的選擇器信息。

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

圖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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章