通过前面对“HttpController的激活”的介绍我们已经知道了ASP.NET Web API通过HttpControllerDescriptor来描述HttpController。对于定义在HttpController中的每一个Action方法则通过一个类型为HttpActionDescriptor的对象来描述,Action方法基本的元数据信息可以在对应的HttpActionDescriptor对象中找到。[本文已经同步到《How ASP.NET Web API Works?》]
目录
HttpActionDescriptor
ReflectedHttpActionDescriptor
ActionNameAttribute
方法名称与支持的HTTP方法
ActionHttpMethodProvider
如下面的代码片断所示,HttpActionDescriptor是一个抽象类型。我们列出了定义其中的部分属性成员,包括代表Action名称的ActionName属性和表示当前HttpConfiguration的Configuration属性,它来源于指定的HttpControllerDescriptor对象的同名属性。属性ControllerDescriptor表示描述所在HttpController的HttpControllerDescriptor对象,而另一个属性ReturnType代表Action方法的返回类型。
1: public abstract class HttpActionDescriptor
2: {
3: //其他成员
4: public abstract string ActionName { get; }
5: public HttpConfiguration Configuration { get; set; }
6: public HttpControllerDescriptor ControllerDescriptor { get; set; }
7: public abstract Type ReturnType { get; }
8: public virtual ConcurrentDictionary<object, object> Properties { get; }
9: public virtual Collection<HttpMethod> SupportedHttpMethods { get; }
10:
11: public abstract Collection<HttpParameterDescriptor> GetParameters();
12: }
和HttpControllerDescriptor类型一样,HttpActionDescriptor也定义了一个的属性Properties属性,其类型为ConcurrentDictionary<object, object>,所以我们可以利用它将任意对象附加到某个HttpActionDescriptor对象上。
在默认情况下每一个Action方法仅仅支持一种唯一的HTTP方法,不过我们通过一些编程技巧让一个Action方法可以同时支持多种HTTP方法。某个Action方法支持的HTTP方法列表可以通过对应HttpActionDescriptor的SupportedHttpMethods来获取。如果一个Action方法仅仅支持HTTP-GET,那么对于一个HTTP-POST的请求,该Action方法将不会被选择。
ASP.NET Web API最终能够执行目标Action方法的前提是能够正确从请求中提取相应的数据并将其绑定到方法对应的参数上。参数绑定的正常执行依赖于一些用于描述参数的元数据,ASP.NET Web API通过HttpParameterDescriptor对象来描述Action方法的参数,而HttpActionDescriptor定了一个具有如下定义的GetParameters方法用于获取描述其所有参数的HttpParameterDescriptor对象列表。
1: public abstract class HttpActionDescriptor
2: {
3: //其他成员
4: public abstract Collection<HttpParameterDescriptor> GetParameters();
5: }
由于基于自定义特性的编程方法在ASP.NET Web API中被广泛采用,ASP.NET Web API框架本身借助于一些应用在Action方法上的特性来控制目标Action的执行,而我们出于对框架扩展的目的也可能将自定义的一些特性应用到相应的Action方法上。为了方便我们获取应用到Action方法上的特性列表,HttpActionDescriptor为我们定义了如下两个GetCustomAttributes<T>方法重载。
1: public abstract class HttpActionDescriptor
2: {
3: //其他成员
4: public virtual Collection<T> GetCustomAttributes<T>() where T: class;
5: public virtual Collection<T> GetCustomAttributes<T>(bool inherit) where T: class;
6: }
HttpActionDescriptor仅仅是一个抽象类型 ,在默认情况下ASP.NET Web API使用的具体HttpActionDescriptor类型为ReflectedHttpActionDescriptor。顾名思义,ReflectedHttpActionDescriptor通过反射的方式来获取用于描述目标Action方法的元数据。
1: public class ReflectedHttpActionDescriptor : HttpActionDescriptor
2: {
3: //其他成员
4: public ReflectedHttpActionDescriptor(HttpControllerDescriptor controllerDescriptor, MethodInfo methodInfo);
5:
6: public override Collection<HttpParameterDescriptor> GetParameters();
7: public override Collection<T> GetCustomAttributes<T>(bool inherit) where T: class;
8:
9: public MethodInfo MethodInfo { get; set; }
10: public override string ActionName { get; }
11: public override Type ReturnType { get; }
12: public override Collection<HttpMethod> SupportedHttpMethods { get; }
13: }
如下面的代码片断所示,在构建一个ReflectedHttpActionDescriptor对象的时候除了需要指定用于描述所在HttpController的HttpControllerDescriptor对象之外,还需要指定描述对应Action方法的MethodInfo对象(属性MethodInfo返回该对象),相关的元数据就来源于此。
ReflectedHttpActionDescriptor利用指定的MethodInfo获得Action方法的返回类型,并将其作为ReturnType属性值。如果Action方法返回类型为void,此ReturnType属性返回Null。在默认的情况下,表示Action名称ActionName属性返回Action方法的名称。如果我们需要指定不同的名称,可以借助于ActionNameAttribute这个特性。
对于一个有效的Action方法来说,其方法名称默认作为Action的名称。采用怎样的URL对于REST这种架构风格来说显得尤为重要,出于简洁性、可读性以及SEO优化等方面的考虑,我们往往需要对方法某个Action方法的URL进行精心设计。如果Action名称会作为请求URL模板的一部分,而Action名称的内容总是对应着Action方法的名称,这将使我们不能对URL进行独立的设计。
为了解决Action名称与方法名称的分离,我们可以在Action方法上应用ActionNameAttribute特性为其指定一个我们希望的名称。ActionNameAttribute的定义如下,表示Action名称的Name属性直接在构造函数中指定。
1: [AttributeUsage(AttributeTargets.Method, AllowMultiple=false, Inherited=true)]
2: public sealed class ActionNameAttribute : Attribute
3: {
4: public ActionNameAttribute(string name);
5: public string Name { get; }
6: }
在DemoController中,我们在Action方法Xxx上通过应用的ActionNameAttribute特性将Action名称设置为“Yyy”。
1: public class DemoController: ApiController
2: {
3: [ActionName(“Yyy”)]
4: public void Xxx();
5: }
定义在某个HttpController中的Action方法默认只支持一种唯一的HTTP方法,并且这个支持的HTTP方法决定于Action方法的名称。具体来说,如果Action方法名称具有如下的前缀(不区分大小写),那么对应的HTTP方法将默认被支持。比如说,如果Action方法被命名为“GetXxx”,那么默认支持HTTP-GET。如果Action方法名称不具有如此前缀,那么默认支持HTTP-POST。
为了让读者朋友们对“Action方法名称决定支持的HTTP方法”这个默认的HTTP方法选择策略具有更加深刻的了解,我们来做一个简单的实例演示。我们定义了如下一个继承自ApiController的DemoController,它具有8个合法的Action方法成员,除了最后一个Other方法之外,其余7个Action方法名称均以相应的HTTP方法名称作为前缀。
1: public class DemoController : ApiController
2: {
3: public void GetXxx() {}
4: public void PostXxx() {}
5: public void PutXxx() {}
6: public void DeleteXxx() {}
7: public void HeadXxx() {}
8: public void OptionsXxx() {}
9: public void PatchXxx() {}
10: public void Other() {}
11: }
我们在一个空的ASP.NET MVC应用中定义了如下一个HomeController。在默认的Action方法Index中,我们针对定义在DemoController的8个方法创建了相应的ReflectedHttpActionDescriptor对象。8个ReflectedHttpActionDescriptor对象对应的Action方法名称和支持的HTTP方法名称列表被转换成一个Dictionary<string, string[]>对象并呈现在默认的View中。
1: public class HomeController : Controller
2: {
3: public ActionResult Index()
4: {
5: HttpControllerDescriptor controllerDescriptor = new HttpControllerDescriptor(new HttpConfiguration(), "demo", typeof(DemoController));
6: Dictionary<string, string[]> supportedHttpMethods = new Dictionary<string, string[]>();
7: foreach (MethodInfo method in typeof(DemoController).GetMethods())
8: {
9: if (method.DeclaringType == typeof(DemoController))
10: {
11: ReflectedHttpActionDescriptor actionDescritor = new ReflectedHttpActionDescriptor(controllerDescriptor, method);
12: supportedHttpMethods.Add(method.Name, actionDescritor.SupportedHttpMethods.Select(httpMethod=>httpMethod.Method).ToArray());
13: }
14: }
15: return View(supportedHttpMethods);
16: }
17: }
如下所示的是Action方法Index对应View的定义,这是一个以IDictionary<string, string[]>对象作为Model的强类型View。在该View中,我们将Action方法的名称和支持的HTTP方法列表以表格的形式呈现出来。
1: @model IDictionary<string, string[]>
2: <html>
3: <head>
4: <title>Action方法名称与支持的HTTP方法</title>
5: </head>
6: <body>
7: <table>
8: <thead>
9: <tr>
10: <th>Action方法名称</th>
11: <th>支持HTTP方法</th>
12: </tr>
13: </thead>
14: <tbody>
15: @foreach (var item in Model)
16: {
17: <tr>
18: <td rowspan="@item.Value.Length">@item.Key</td>
19: <td>@item.Value[0]</td>
20: </tr>
21: for (int i = 1; i < item.Value.Length; i++)
22: {
23: <tr><td>@item.Value[i]</td></tr>
24: }
25: }
26: </tbody>
27: </table>
28: </body>
29: </html>
直接运行该程序后会在浏览器中得到如右图所示的输出结果。我们可以清楚地看到定义在DemoController的前面7个Action方法支持的HTTP方法对应于其方法名称采用的前缀,而最后一个Action方法Other则采用默认的HTTP-POST作为唯一支持的HTTP方法。
这种由Action方法名来决定它所支持的HTTP方法的策略虽然在一定程度上是我们的编程变得简单,但是在很多情况下与我们的期望是不相符的。比如我们定义一个名为RetrieveContacts的Action方法来获取相应的联系人,它所支持的HTTP方法应该是HTTP-GET,但是按照实际支持的HTTP方法却是HTTP-POST。
除此之外,默认情况下一个Action方法仅仅支持一种唯一的HTTP方法,但是很多情况下我们希望一个Action方法能够支持多种HTTP方法。比如我们定义一个名为UpdateContact的Action方法,它既支持针对新联系人的添加也支持现有联系人的修改,所以我们希望它同时支持HTTP-POST和HTTP-PUT这两种HTTP方法。这两种情况下都可以通过在Action方法上应用相应的ActionHttpMethodProvider特性来解决。
顾名思义,ActionHttpMethodProvider被相应的Action用于提供所支持的HTTP方法,所有的ActionHttpMethodProvider实现了System.Web.Http.Controllers.IActionHttpMethodProvider接口。如下面的代码片断所示,IActionHttpMethodProvider接口定义了一个唯一的只读属性HttpMethods返回其提供的HTTP方法集合。
1: public interface IActionHttpMethodProvider
2: {
3: Collection<HttpMethod> HttpMethods { get; }
4: }
ActionHttpMethodProvider在以特性的方式应用于相应的Action方法上。ASP.NET Web API为我们定义了两种实现了IActionHttpMethodProvider接口的特性,其中一个是具有如下定义的AcceptVerbsAttribute。当我们将AcceptVerbsAttribute特性应用到某个Action方法上的时候,可以直接以字符串的形式在构造函数中指定它所支持的HTTP方法名称(“GET”、“POST”、“PUT”、“DELETE”、“HEAD”、“OPTIONS”、“PATCH”等,指定的HTTP方法名称是不区分大小写的)。
1: [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
2: public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider, ...
3: {
4: //其他成员
5: public AcceptVerbsAttribute(params string[] methods);
6: public Collection<HttpMethod> HttpMethods { get; }
7: }
在如下所示的代码片断中,我们将AcceptVerbsAttribute特性应用到Action方法Update中,使之可以同时支持HTTP-PUT和HTTP-POST两种HTTP方法的请求。
1: public class ContactController : ApiController
2: {
3: //其他成员
4: [AcceptVerbsAttribute("PUT","POST")]
5: public void Update(Contact contact)
6: {
7: //省略实现
8: }
9: }
如果使用AcceptVerbsAttribute特性,我们只能以字符串的形式指定目标Action方法所支持的HTTP方法,为了避免指定错误HTTP方法名称,ASP.NET Web API为我们定义了另一种类型的ActionHttpMethodProvider特性,即具有如下定义的HttpVerbAttribute。
1: public abstract class HttpVerbAttribute : Attribute, IActionHttpMethodProvider, ...
2: {
3: //其他成员
4: protected HttpVerbAttribute(HttpMethod httpMethod);
5: public Collection<HttpMethod> HttpMethods { get; }
6: }
通过HttpVerbAttribute的定义可以看出它是一个抽象类型,针对上面我们介绍的7种常用的HTTP方法,ASP.NET Web API通过继承HttpVerbAttribute定义了相应的特性类型。这些针对具体某种HTTP方法的特性类型均定义在System.Web.Http命名空间下,它们分别是:
上面我们通过应用AcceptVerbsAttribute特性实现了目标Action方法对HTTP-POST和HTTP-POST这两种HTTP方法的支持,我们可以以如下的方式在目标Action方法上应用HttpPutAttribute和HttpPostAttribute实现相同的效果。
1: public class ContactController : ApiController
2: {
3: //其他成员
4: [HttPut]
5: [HttpPost]
6: public void Update(Contact contact)
7: {
8: //省略实现
9: }
10: }
上面我们通过实现演示了默认情况下基于Action方法名称的HTTP方法支持策略,现在我们按照如下的方式将HttpGetAttribute和HttpPostAttribute应用到定义在DemoController中的8个Action方法上,使所有的Action方法均提供对HTTP-GET和HTTP-POST请求的支持。
1: public class DemoController : ApiController
2: {
3: [HttpGet]
4: [HttpPost]
5: public void GetXxx() {}
6:
7: [HttpGet]
8: [HttpPost]
9: public void PostXxx(){}
10:
11: [HttpGet]
12: [HttpPost]
13: public void PutXxx(){}
14:
15: [HttpGet]
16: [HttpPost]
17: public void DeleteXxx(){}
18:
19: [HttpGet]
20: [HttpPost]
21: public void HeadXxx(){}
22:
23: [HttpGet]
24: [HttpPost]
25: public void OptionsXxx() {}
26:
27: [HttpGet]
28: [HttpPost]
29: public void PatchXxx() {}
30:
31: [HttpGet]
32: [HttpPost]
33: public void Other() {}
34: }
再次运行该程序后会在浏览器中呈现出如下图所示的结果。我们可以清楚地看到定义在DemoController中的8个Action方法均提供对HTTP-GET和HTTP-POST这两种HTTP方法的支持。