[水煮 ASP.NET Web API2 方法论](3-1)集中式路由

时间:2021-10-30 16:29:24

问题

怎样集中的定义路由

解决方案

通过调用 HttpRouteCollectionExtension 类中的 MapHttpRoute 扩展方法在 HttpRouteCollection 中定义路由,可以通过 HttpConfiguration 对象调用。

最基础的使用就是定义一个非常通用的路由模板,他会通过 {controller} 占位符匹配所有的 Controller。如代码片段 3-1 所示。

代码片段 3-1. ASP.NET WEB API 默认定义的路由以及一个简单的 Controller

 config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new {id = RouteParameter.Optional}
); public class OrdersController : ApiController
{
public Order Get(int id)
{
// 忽略逻辑
}
}

在路由模板中,可以定义自己的占位符,如代码片段 3-1 所示的 {id}。他与 Action 中的参数名称相匹配匹配,ASP.NET WEB API 会从 request 中提取出相应的值,传入 Action 方法中。也就是,代码片段 3-1 中 OrdersController 的 Get请求方法的这种情

况。

工作原理

从最初版本开始,ASP.NET WEB API 就一直使用集中式路由维护路由表,这和 MVC 如出一辙。

ASP.NET WEB API 定义了很多 MapHttpRoute 的变种。所需参数最少的一个方法,只需要一个路由模板和一个路由名称。

 public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name,
string routeTemplate)

除声明简单基础路由以外的方式,也可以通过默认值和约束,或者设置每个路由消息处理程序,将会在下面的章节介绍这个。所有的这些操作都是通过 Map HttpRoute 的重载方法实现的。

 public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name,
string routeTemplate, object defaults) public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name,
string routeTemplate, object defaults, object constraints) public static IHttpRoute MapHttpRoute(this HttpRouteCollection routes, string name,
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

 public class BadOrdersController : ApiController
{
[HttpGet]
public Order FindById(int id)
{
// 忽略逻辑
} public Order GetById(int id)
{
// 忽略逻辑
} public Order Get(int id)
{
// 忽略逻辑
}
}

同时我们需要注意,定义复杂、多等级、嵌套路由的时候,集中式路由变得有点麻烦。考虑如下路由

  • 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 使用集中式路由配置嵌套路由

 public class TeamsController : ApiController
{
public Team GetTeam(int id)
{
// 忽略逻辑
}
public IEnumerable<Team> GetTeams()
{
// 忽略逻辑
}
public IEnumerable<Player> GetPlayers(int teamId)
{
// 忽略逻辑
}
} config.Routes.MapHttpRoute(
name: "players",
routeTemplate: "api/teams/{teamid}/players",
defaults: new {controller = "teams"}
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

集中式路由的主要问题是,特殊路由的定义,仅仅是处理特殊的 Controller 特殊的 Action。通过定义特殊路由并添加到一般路由之前,在人为干预下短路了路由匹配。

这种处理并不是最理想,当应用程序变大之后,会有更复杂的、多层级的路由,可能就要在集中式路由中痛苦的挣扎(路由维护和调试)。更好的选择就是使用直接式路由,下一篇 3-2 定义直接路由,以及后面介绍路由的时候都会涉及直接式路由。