问题
怎样集中的定义路由
解决方案
通过调用 HttpRouteCollectionExtension 类中的 MapHttpRoute 扩展方法在 HttpRouteCollection 中定义路由,可以通过 HttpConfiguration 对象调用。
最基础的使用就是定义一个非常通用的路由模板,他会通过 {controller} 占位符匹配所有的 Controller。如代码片段 3-1 所示。
代码片段 3-1. ASP.NET WEB API 默认定义的路由以及一个简单的 Controller
1 config.Routes.MapHttpRoute( 2 name: "DefaultApi", 3 routeTemplate: "api/{controller}/{id}", 4 defaults: new {id = RouteParameter.Optional} 5 ); 6 7 8 9 public class OrdersController : ApiController 10 { 11 public Order Get(int id) 12 { 13 // 忽略逻辑 14 } 15 }
在路由模板中,可以定义自己的占位符,如代码片段 3-1 所示的 {id}。他与 Action 中的参数名称相匹配匹配,ASP.NET WEB API 会从 request 中提取出相应的值,传入 Action 方法中。也就是,代码片段 3-1 中 OrdersController 的 Get请求方法的这种情
况。
工作原理
从最初版本开始,ASP.NET WEB API 就一直使用集中式路由维护路由表,这和 MVC 如出一辙。
ASP.NET WEB API 定义了很多 MapHttpRoute 的变种。所需参数最少的一个方法,只需要一个路由模板和一个路由名称。
1 public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, 2 string routeTemplate)
除声明简单基础路由以外的方式,也可以通过默认值和约束,或者设置每个路由消息处理程序,将会在下面的章节介绍这个。所有的这些操作都是通过 Map HttpRoute 的重载方法实现的。
1 public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, 2 string routeTemplate, object defaults) 3 4 public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, 5 string routeTemplate, object defaults, object constraints) 6 7 public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name, 8 string routeTemplate, object defaults, object constraints, HttpMessageHandler handler)
通过路由默认值,可以直接定位到路由相关的 Controller;可是,这样的特定路由需要定义在通用路由之前,也就是代码片段 3-1 的路由之前。原因就是,路由是顺序匹配的,路由在选定处理一个 HTTP 请求的时候,是在每次请求的时候,扫描路由集合中的所有路由,选用第一个被匹配的路由模板。简单的说,越是特殊的路由定义越靠前,越是通用的路由定义越靠后。
在 Self-hosting 中,ASP.NET WEB API 是以 IDictionary<stirng, IHttpRoute> 的形式在维护路由,Idictionary<string, IHttpRoute> 是在 HttpRouteCollection 类中。在 Web host 中也提供了 System.Web.Routing.RouteCollection 的扩展方法,因此,可以直接在这个里面定义 ASP.NET WEB API 路由。
RouteTable.Routes.MapHttpRoute("DefaultApi", "api/{controller}")
如果使用 ASP.NET 运行时 host WEB API,无论使用什么扩展方法声明路由,都会被添加到同一个 RouteTable 中。内部是通过一个叫做 HostedHttpRouteCollection 的类来实现的,这个类是 HttpRouteCollection 的子类,而不是在字典(比如 self-host)中维护路由,所有转发路由都是查找 System.Web.RouteCollection。
ASP.NET WEB API 与 ASP.NET MVC 是不一样的,API 使用的是基于 HTTP 谓词匹配来处理一个 Controller 请求。换句话说,框架会根据 HTTP 的东西来选择一个 Action 处理请求,选择的逻辑如下:
- 通过方法名推断 HTTP 谓词,如果名字类似于 PostOrder、GetById 等等。
- 通过 Action 属性推断 HTTP 谓词。
- Action 参数定义与路由模板必须匹配。
典型的 ASP.NET WEB API 路由是指向资源的。可以通过 HTTP 谓词调用,还需要除了 Action 之外的参数。如代码片段 3-1 所示的默认路由,可以匹配如下请求:
- GET myapi.com/api/orders
- POST myapi.com/api/inovice
- PUT myapi.com/api/invice/2
小提示 在 ASP.NET WEB API 是可以使用 RPC 的,具体细节会在 3-6 介绍。
代码演示
ASP.NET WEB API 是基于 HTTP 谓词进行分发逻辑的,不像 ASP.NET MVC,很容易出现 AmbiguousMatchEception。例如,可以设想一些使用代码片段 3-1 的路由的例子。
如代码片段 3-2 所示的例子,这三个方法都是可以处理相同的 GET 请求,如,/api/orders/1.
代码片段 3-2.
代码片段 3-2. ASP.NET WEB API Controller
1 public class BadOrdersController : ApiController 2 { 3 [HttpGet] 4 public Order FindById(int id) 5 { 6 // 忽略逻辑 7 } 8 9 public Order GetById(int id) 10 { 11 // 忽略逻辑 12 } 13 14 public Order Get(int id) 15 { 16 // 忽略逻辑 17 } 18 }
同时我们需要注意,定义复杂、多等级、嵌套路由的时候,集中式路由变得有点麻烦。考虑如下路由
- GET myapi.com/api/teams
- GET myapi.com/api/teams/1
- GET myapi.com/api/teams/1/players
使用集中式路由,可以在 Controller 中使用如下三个方法;然而,我们必须注意路由之间冲突的问题,因为有两个 GET 方法都是只使用了一个 int 的参数。如代码片段 3-3 所示。有一个特殊的路由指出了 URI 中包含 /players/ 段会被匹配到,而且这个路由定义在通用路由之前。
代码片段 3-3 使用集中式路由配置嵌套路由
1 public class TeamsController : ApiController 2 { 3 public Team GetTeam(int id) 4 { 5 // 忽略逻辑 6 } 7 public IEnumerable<Team> GetTeams() 8 { 9 // 忽略逻辑 10 } 11 public IEnumerable<Player> GetPlayers(int teamId) 12 { 13 // 忽略逻辑 14 } 15 } 16 17 18 19 config.Routes.MapHttpRoute( 20 name: "players", 21 routeTemplate: "api/teams/{teamid}/players", 22 defaults: new {controller = "teams"} 23 ); 24 config.Routes.MapHttpRoute( 25 name: "DefaultApi", 26 routeTemplate: "api/{controller}/{id}", 27 defaults: new { id = RouteParameter.Optional } 28 );
集中式路由的主要问题是,特殊路由的定义,仅仅是处理特殊的 Controller 特殊的 Action。通过定义特殊路由并添加到一般路由之前,在人为干预下短路了路由匹配。
这种处理并不是最理想,当应用程序变大之后,会有更复杂的、多层级的路由,可能就要在集中式路由中痛苦的挣扎(路由维护和调试)。更好的选择就是使用直接式路由,下一篇 3-2 定义直接路由,以及后面介绍路由的时候都会涉及直接式路由。