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