ASP.NET Web API 2.0新特性:Attribute Routing[上篇]
对于一个针对ASP.NET Web API的调用请求来说,请求的URL和对应的HTTP方法的组合最终决定了目标HttpController的类型和定义其中的目标Action方法。两者之间的映射是通过URL路由来完成的,ASP.NET Web API路由系统提供了一种便捷的方式使我们可以在统一的地方注册适用于所有HttpController的路由。
如果我们能够直接针对目标Action方法进行路由注册,那么我们就能够对路由规则进行细粒度的控制。从设计角度来讲,Web API采用REST架构风格,其URL更多是对目标资源的反映,定义在同一个HttpController中的众多Action方法体现在对一组相关资源的操作。目标资源往往具有一定地层次结构,RESTful的URL应该尽可能地反映这种层次目标资源的这种层次结构,能否针对具体的Action方法进行路由映射往往决定了我们能否设计出真正具有REST风格的URL。为此,ASP.NET Web API 2.0对现有路由系统进行了扩展,提供了一种基于特性的路由是我们可以直接将路由直接注册到具体的Action方法上。[本文已经同步到《How ASP.NET Web API Works?》]
目录
HttpRouteInfoProvider特性
开启针对特性的路由
基本路由注册
让URL模板能够尽可能反映资源的层次结构
为路由变量设置约束
通配符路由变量
缺省路由变量
设置URL前缀
指定路由名称
生成HttpRoute在路由表中的顺序
HttpRouteInfoProvider特性
我们将应用到目标Action方法上直接进行路由注册的特性称为HttpRouteInfoProvider特性,因为它实现了System.Web.Http.Routing.IHttpRouteInfoProvider接口。我们在上面已经提到过了,这种基于特性的路由注册方式仅仅是对ASP.NET Web API现有路由系统的扩展,应用到目标方法上用于路由注册的特性最终会被转换成相应的HttpRoute对象。顾名思义,HttpRouteInfoProvider特性旨在提供最终创建这个HttpRoute对象的相关信息。
HttpRouteInfoProvider提供的路由信息集中体现在IHttpRouteInfoProvider接口的几个属性。如下面的代码片断所示,IHttpRouteInfoProvider定义了 三个只读属性,其核心自然是通过属性RouteTemplate表示的URL模板。它的属性RouteName表示最终创建的HttpRoute对象的名称,另一个属性RouteOrder则决定了该HttpRoute对象在路由表中的位置,这个位置最终决定了其被选择的优先级。
1: public interface IHttpRouteInfoProvider
2: {
3: string RouteName { get; }
4: int RouteOrder { get; }
5: string RouteTemplate { get; }
6: }
读者朋友们可能对IHttpRouteInfoProvider接口不太熟悉,但是对实现它的两个特性应该已经很熟悉了,它们分别是AcceptVerbsAttribute和HttpVerbAttribute。在前面介绍“Action的选择”中我们将这两个特性视为用于选择目标Action方法所支持的HTTP方法的ActionHttpMethodProvider特性来介绍的,其实它们除了实现IActionHttpMethodProvider接口之外,还实现了接口IHttpRouteInfoProvider。
AcceptVerbsAttribute
如下所示的AcceptVerbsAttribute的完整定义,当我们将它应用到目标Action方法上的时候,除了以构造函数参数的形式指定所支持的HTTP方法列表(以字符串的形式)之外,还可以指定RouteName、RouteOrder和RouteTemplate属性值以注册一个针对目标方法的路由。
1: [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
2: public sealed class AcceptVerbsAttribute : Attribute, IActionHttpMethodProvider, IHttpRouteInfoProvider
3: {
4: public AcceptVerbsAttribute(params string[] methods);
5:
6: public Collection<HttpMethod> HttpMethods { get; }
7: public string RouteName { get; set; }
8: public int RouteOrder { get; set; }
9: public string RouteTemplate { get; set; }
10: }
对于如上所示的AcceptVerbsAttribute的定义,有一个细节我们只得注意:应用在该类型的AttributeUsageAttribute特性将AllowMultiple属性设置为True,意味着多个AcceptVerbsAttribute特性可以同时应用到同一个Action方法上。对于路由映射来说就意味着我们可以为同一个Action方法注册多个路由,这使我们可以采用不同的URL来访问同一个Action方法。
HttpVerbAttribute
AcceptVerbsAttribute和HttpVerbAttribute的不同之处在于前者可以通过字符串的形式指定多个支持的HTTP方法,而后者仅仅针对某个具体的HTTP方法。如下所示的是HttpVerbAttribute的完整定义,我们从中可以看出表示URL模板的只读属性RouteTemplate需要在构造函数中指定,而另外两个属性RouteName和RouteOrder则是可读可写的。
1: public abstract class HttpVerbAttribute : Attribute, IActionHttpMethodProvider, IHttpRouteInfoProvider
2: {
3: protected HttpVerbAttribute(HttpMethod httpMethod);
4: protected HttpVerbAttribute(HttpMethod httpMethod, string routeTemplate);
5:
6: public Collection<HttpMethod> HttpMethods { get; }
7: public string RouteName { get; set; }
8: public int RouteOrder { get; set; }
9: public string RouteTemplate { get; }
10: }
针对7种常用的HTTP方法,ASP.NET Web API定义了7个继承自HttpVerbAttribute的类型,它们分别是具有如下定义的HttpGetAttribute、HttpPostAttribute、HttpPutAttribute、HttpDeleteAttribute、HttpPatchAttribute、HttpHeadAttribute和HttpOptionsAttribute。我们可以看到这7个具体的HttpVerbAttribute均定义了两个构造函数重载,如果需要使用它们来注册路由,需要在构造函数中指定相应URL模板。
1: [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
2: public sealed class HttpGetAttribute : HttpVerbAttribute
3: {
4: public HttpGetAttribute();
5: public HttpGetAttribute(string routeTemplate);
6: }
7:
8: [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
9: public sealed class HttpPostAttribute : HttpVerbAttribute
10: {
11: public HttpPostAttribute();
12: public HttpPostAttribute(string routeTemplate);
13: }
14:
15: [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
16: public sealed class HttpPutAttribute : HttpVerbAttribute
17: {
18: public HttpPutAttribute();
19: public HttpPutAttribute(string routeTemplate);
20: }
21:
22: [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
23: public sealed class HttpDeleteAttribute : HttpVerbAttribute
24: {
25: public HttpDeleteAttribute();
26: public HttpDeleteAttribute(string routeTemplate);
27: }
28:
29: [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
30: public sealed class HttpPatchAttribute : HttpVerbAttribute
31: {
32: public HttpPatchAttribute();
33: public HttpPatchAttribute(string routeTemplate);
34: }
35:
36: [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
37: public sealed class HttpHeadAttribute : HttpVerbAttribute
38: {
39: public HttpHeadAttribute();
40: public HttpHeadAttribute(string routeTemplate);
41: }
42:
43: [AttributeUsage(AttributeTargets.Method, AllowMultiple=true, Inherited=true)]
44: public sealed class HttpOptionsAttribute : HttpVerbAttribute
45: {
46: public HttpOptionsAttribute();
47: public HttpOptionsAttribute(string routeTemplate);
48: }
在完成了用于进行路由注册的两个HttpRouteInfoProvider特性(AcceptVerbsAttribute和HttpVerbAttribute)之后,我们以实例演示的形式介绍如何利用它们进行针对目标Action方法的路由映射。简单起见,我们只选择使用针对单一HTTP方法的HttpVerbAttribute特性来注册我们需要的路由。
开启针对特性的路由
相对于针对特定的路由(Attribute-based Routing),我们将传统的路由称为基于约定的路由(Convention-based Routing)。在默认的情况下针对特定的路由机制是关闭的,我们需要调用HttpConfiguration具有如下定义的扩展方法MapHttpAttributeRoutes打开这个开关。
1: public static class HttpConfigurationExtensions
2: {
3: //其他成员
4: public static void MapHttpAttributeRoutes(this HttpConfiguration configuration);
5: public static void MapHttpAttributeRoutes(this HttpConfiguration configuration, HttpRouteBuilder routeBuilder);
6: }
如上面的代码片断所示,这个扩展方法具有两个重载,其中一个方法接受一个类型为HttpRouteBuilder的参数。顾名思义,HttpRouteBuilder的作用在于根据应用的HttpRouteInfoProvider特性创建对应的HttpRoute。
基本路由注册
假设有一家专门负责销售影碟的网店,我们为它开发了一套Web API来提供影片的信息,同时供后台应用进行影片资源的维护。简单起见,我们只定义了如下一个表示影片的类型Movie,它仅仅封装了一部电影包含片名、类型、主演、导演、发行日期、对白语言和故事摘要在内的基本信息,属性ID作为它的唯一标识。
1: public class Movie
2: {
3: public Guid Id { get; set; }
4: public string Name { get; set; }
5: public IEnumerable<string> Genres { get; set; }
6: public IEnumerable<string> Starring { get; set; }
7: public string Director { get; set; }
8: public DateTime ReleaseDate { get; set; }
9: public string Language {get; set;}
10: public string Story { get; set; }
11: }
我们将用于操作影片资源的基本操作定义在如下一个名为MoviesController的HttpController中,5个基本的Action方法上均应用了针对某个HTTP方法的HttpVerbAttribute特性并指定了相应的URL模板。用于根据ID获取对应影片信息的Action方法GetMovieById采用的URL模板为“api/movies/{id}”,另一个Action方法GetMovieByStarring返回由指定的某个演员主演的影片列表,它采用的URL模板为“api/movies/starring/{starring}”。这两个Action方法均仅仅提供针对HTTP-GET请求的唯一支持,URL模板中定义的路由变量(“{id}”和“{starring}”)均映射到目标方法的同名参数上。
1: public class MoviesController: ApiController
2: {
3: [HttpGet("api/movies/{id}")]
4: public Movie GetMovieById(Guid id);
5:
6: [HttpGet("api/movies/starring/{starring}")]
7: public IEnumerable<Movie> GetMovieByStarring(string starring);
8:
9: [HttpPost("api/movies")]
10: public void Add(Movie movie);
11:
12: [HttpPut("api/movies")]
13: public void Update(Movie movie);
14:
15: [HttpDelete("api/movies/{id}")]
16: public void Delete(string id);
17: }
其他三个Action方法Add、Update和Delete分别进行添加、修改和删除操作,我们分别在它们上面应用了HttpPostAttribute、HttpPutAttribute和HttpDeleteAttribute来控制它们支持的HTTP方法和指定URL模板。
因为用于注册路由的HttpRouteInfoProvider特性直接应用到目标Action方法上,当ASP.NET Web API根据它们生成相应HttpRoute的时候可以解析出目标HttpController和Action的名称,所以我们使用HttpRouteInfoProvider特性进行路由注册的时候无需指定对应的路由变量(“{controller}”和“{action}”)。
针对定义在MoviesController中支持HTTP-GET请求的两个Action方法GetMovieById和GetMovieByStarring,我们可以直接通过浏览器访问匹配的URL来获取相应的影片信息。如右图所示,我们通过地址“/api/movies/b4e0bc49-568d-402b-acf0-37b65008723f”获取ID为“b4e0bc49-568d-402b-acf0-37b65008723f”的 影片。通过地址“/api/movies/starring/pacino”得到的是由Pacino主要的影片列表。
让URL模板能够尽可能反映资源的层次结构
我们说Web API操作的目标资源具有的层次化结构往往不是指的资源本身具有的物理结构,而只要体现在资源针对不同的调用请求而具有的逻辑结构。就我们的例子来说,目标资源就是一组影片列表而已,其数据本身并不具有复杂的物理结构,其层次化的逻辑结构体现在针对不同条件组合的查询。我们需要针对资源的逻辑结构设计对应的URL,比如:
- 获取由Pacino主演的剧情片:/api/movies/starring/pacino/drama
- 获取由Cameron导演的爱情片:/api/movies/director/cameron/romance
针对如上这两种具体的需求,我们可以按照在MoviesController中添加如下两个Action方法。方法GetMovieByStarringAndGenre采用的URL模板分别为“api/movies/starring/{starring}/{genre}”而方法GetMovieByDirectorAndGenre 采用的URL为“api/movies/director/{director}/{genre}”。
1: public class MoviesController : ApiController
2: {
3: //其他成员
4: [HttpGet("api/movies/starring/{starring}/{genre}")]
5: public IEnumerable<Movie> GetMovieByStarringAndGenre(string starring, string genre);
6:
7: [HttpGet("api/movies/director/{director}/{genre}")]
8: public IEnumerable<Movie> GetMovieByDirectorAndGenre(string director, string genre);
9: }
针对注册在这两个Action方法上的路由,我们可以采用匹配的URL获取相应的影片列表。如右图6-2所示,我们直接利用浏览器发送请求来调用上面定义的两个Action方法。我们采用地址“/api/movies/starring/pacino/crime”获取由Pacino主演的犯罪类型的影片列表,而另一个地址“/api/movies/director/brest/drama”则用于获取由Brest导演的剧情片列表。
通过上面的介绍我们知道两种类型的HttpRouteInfoProvider特性(AcceptVerbsAttribute和HttpVerbAttribute)均具有这样的特性:多个同一类型的特性可以同时应用到相同的Action方法上。如果我们按照如下的方式将针对不同条件组合(主演、导演和类型)的查询均定义在同一个FindMovies方法上,然后通过应用多个HttpGetAttribute特性注册多个采用不同URL模板的路由,那么是否也能解决上面这个问题呢?
1: public class MoviesController : ApiController
2: {
3: //其他成员
4: [HttpGet("api/movies/starring/{starring}")]
5: [HttpGet("api/movies/starring/{starring}/{genre}")]
6: [HttpGet("api/movies/director/{director}/{genre}")]
7: public IEnumerable<Movie> FindMovies(string starring, string director, string genre);
8: }
实际上这个Action方法FindMovies是没有办法按照应用在它上面的HttpGetAttribute特性指定的URL模板进行调用的。如左图所示,我们在浏览器中采用模式匹配的三个URL来调用Web API,但无一例外都得到一个状态为“404, Not Found”的响应,并提示根据请求的URL无法找在目标HttpController中找到一个匹配的Action。
既然我们我们已经明确将相应的路由通过HttpGetAttribute特性注册到Action方法FindMovies上了,但是为何ASP.NET Web API却说根据请求URL(该URL与注册路由的URL模式相匹配)在MoviesController中找不到匹配的Action呢?其实《ASP.NET Web API是如何根据请求选择Action的?》已经回答了这个问题。
在《ASP.NET Web API是如何根据请求选择Action的?》一文中,我们介绍了ASP.NET Web API针对请求选择目标Action会经历4轮筛选,其中第3轮筛选出的候选Action必须满足这样的条件:Action方法中定义的由URL提供的非缺省参数必须存在于当前请求的URL中。对于我们这个例子来说,三个请求采用的URL都不能完整地提供定义在FindMovies方法上的所有三个参数(starring、director和genre)。
那么既然“非缺省参数”才需要由请求的URL来提供,那么如果我们将参数定义成缺省参数是否可以解决这个问题呢?为此我们按照如下的方式将FindMovies方法的三个参数均定义成默认值为空字符串的可缺省参数。
1: public class MoviesController : ApiController
2: {
3: //其他成员
4: [HttpGet("api/movies/starring/{starring}")]
5: [HttpGet("api/movies/starring/{starring}/{genre}")]
6: [HttpGet("api/movies/director/{director}/{genre}")]
7: public IEnumerable<Movie> FindMovies(string starring = "", string director = "", string genre = "");
8: }
我们再次利用浏览器采用相同的URL来调用定义在MoviesController中的这个Action方法FindMovies,会得到如右图所示的输出结果,可见将Action方法的参数定义成可缺省的参数可以帮助ASP.NET Web API找到对应我们期望的目标参数,我们在项目中也可以利用这样的变成技巧。
出处:http://artech.cnblogs.com/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。