2 路由

这篇文章描述了ASP.NET Web API如何将HTTP请求发送(路由)到控制器。

备注:如果你对ASP.NET MVC很熟悉,你会发现Web API路由和MVC路由非常相似。主要区别是Web API使用HTTP方法来选择动作(action),而不是URI路径。你也可以在Web API中使用MVC风格的路由。这篇文章不需要ASP.NET MVC的任何知识。

路由表

在ASP.NET Web API中,控制器是一个用于处理HTTP请求的类。控制器中的公共方法被称为动作方法或简单动作。当Web API框架收到请求时,它会将该请求路由到相应的动作中。

为了确定哪个动作该被执行,框架就会使用本节将讲解的路由表。Visual Studio的Web API项目模板就创建了一个默认的路由表:

  1. routes.MapHttpRoute(
  2. name: "API Default",
  3. routeTemplate: "api/{controller}/{id}",
  4. defaults: new { id = RouteParameter.Optional }
  5. );

这个路由被定义在App_Start目录下的WepApiConfig.cs文件中。

这里写图片描述 图片 2.1 这里写图片描述

路由表中的每条记录都包含了一个路由模板。Web API的默认路由模板是“api/{controller}/{id}”。在这个模板中,”api”是一个字面路径字段,而{controller}和{id}都是占位符变量。

当Web API框架收到了HTTP请求时,它将会尽力匹配URI到路由表中的路由模板的其中一个。如果没有路由被匹配到,客户端就会收到404错误。例如,以下URI会匹配到默认路由:

  • /api/contacts
  • /api/contacts/1
  • /api/products/gizmo1

然而,以下URI不会匹配到,因为它缺乏“api”字段。

  1. /contacts/1

备注:在路由中使用“api”的原因是为了避免和ASP.NET MVC的路由冲突。也就是说,你可以使用”/contacts”匹配到MVC的路由,使用“api/contacts”匹配到Web API的路由。当然了,如果你不喜欢这种约定,你也可以修改默认路由表。

一旦某个路由匹配到了,Web API就会选择相应的控制器及动作:

  • 为了找到控制器,Web API将“Controller”添加到{controller}变量上。
  • 为了找到动作,Web API会遍历HTTP方法,然后查找一个其名字以HTTP方法的名字开头的动作。例如,有一个GET请求,Web API会查找以“Get….”开头的动作,比如”GetContact”或”GetAllContacts”。这种方式仅仅适用于GET、POST、PUT和DELETE方法。你可以通过在你的控制器中使用属性来启用其他HTTP方法。将晚些看到一个示例(超链接到本章的第三节……
  • 路由模板的其他占位符变量,比如{id},会被映射到动作的参数。

让我们来看一个示例。假定你定义了如下的控制器:

  1. public class ProductsController : ApiController
  2. {
  3. public void GetAllProducts() { }
  4. public IEnumerable<Product> GetProductById(int id) { }
  5. public HttpResponseMessage DeleteProduct(int id){ }
  6. }

这里是一些可能的HTTP请求,以及相应的得到执行的动作:

HTTP Method URI Path Action Parameter
GET api/products GetAllProducts (none)
GET api/products/4 GetProductById 4
DELETE api/products/4 DeleteProduct 4
POST api/products (no match)

注意URI的{id}字段,如果存在,它会被映射到动作的id参数中。在本例,控制器定义了两个GET方法,其中一个包含id参数,而另一个不包含id参数。

同样的,注意到POST请求会失败,因为控制器中并没有定义”POST…”方法。

路由偏差(Routing Variations)

前一节描述了ASP.NET Web API的基本路由机制。本节将开始描述一些变化。

HTTP方法

除了使用这些HTTP方法的命名约定,你也可以通过用HttpGet、HttpPut、HttpPost或HttpDelete属性来赋予这些动作来具体地为每个动作设定HTTP方法。

在下面这个例子中,FindProduct方法被映射到GET请求:

  1. public class ProductsController : ApiController
  2. {
  3. [HttpGet]
  4. public Product FindProduct(id) {}
  5. }

为了让一个动作支持多个HTTP方法,或支持除GET、PUT、POST和DELETE之外的HTTP方法,你可以使用AcceptVerbs属性,它以一个HTTP方法列表为参数。

  1. public class ProductsController : ApiController
  2. {
  3. [AcceptVerbs("GET", "HEAD")]
  4. public Product FindProduct(id) { }
  5.  
  6. // WebDAV method
  7. [AcceptVerbs("MKCOL")]
  8. public void MakeCollection() { }
  9. }

通过动作名进行路由

有了默认的路由模板,Web API使用HTTP方法来选择动作。然而,你也可以创建一个将动作名包含在URI中的路由表。

  1. routes.MapHttpRoute(
  2. name: "ActionApi",
  3. routeTemplate: "api/{controller}/{action}/{id}",
  4. defaults: new { id = RouteParameter.Optional }
  5. );

在这个路由模板中,{action}参数在控制器中命名了一个动作方法。在这种风格的路由中,应使用属性来指定允许的HTTP方法。例如,假定你的控制器有了以下方法:

  1. public class ProductsController : ApiController
  2. {
  3. [HttpGet]
  4. public string Details(int id);
  5. }

在这种情况下,对于“api/products/details/1”的GET请求被被映射到Details方法。这种风格的路由和ASP.NET MVC很接近,并且可能适合于RPC风格的API。

你可以通过ActionName属性来重写动作名。在接下来的例子中,存在两个都映射到”api/products/thumbnail/id”的动作。其中一个支持GET,另一个支持POST:

  1. public class ProductsController : ApiController
  2. {
  3. [HttpGet]
  4. [ActionName("Thumbnail")]
  5. public HttpResponseMessage GetThumbnailImage(int id);
  6.  
  7. [HttpPost]
  8. [ActionName("Thumbnail")]
  9. public void AddThumbnailImage(int id);
  10. }

无动作(Non-Actions)

为了阻止一个方法被当作动作来执行,可以使用NonAction属性。这会框架指明该方法并非一个动作,即使是它可能匹配到路由规则。

  1. // Not an action method.
  2. [NonAction]
  3. public string GetPrivateData() { ... }

这篇文章描述了ASP.NET Web API如何将HTTP请求路由到控制器上的特定动作。

备注:想要了解关于路由的高层次概述,请查看Routing in ASP.NET Web API。

这篇文章侧重于路由过程的细节。如果你创建了一个Web API项目并且发现一些请求并没有按你预期得到相应的路由,希望这篇文章有所帮助。

路由有以下三个主要阶段:

  • 将URI匹配到路由模板
  • 选择一个控制器
  • 选择一个动作

你可以用自己的习惯行为来替换其中一些过程。在本文中,我会描述默认行为。在结尾,我会指出你可以自定义行为的地方。

路由模板(Route Templates)

路由模板看起来和URI路径非常相似,但是它能包含用大括号指明的占位符。

  1. "api/{controller}/public/{category}/{id}"

当你创建了一个路由,你为一些或全部占位符提供默认的值:

  1. defaults: new { category = "all" }

你也可以提供一些约束(constraints),它限制了URI字段如何才能匹配一个占位符:

  1. constraints: new { id = @"\d+" } // Only matches if "id" is one or more digits.

框架会尽力将URI路径中的字段匹配到模板中。模板中的文字必须准确匹配。一个占位符可以匹配多个变量,除非你指定了约束。框架不会匹配URI的其他部分,比如主机名或查询参数。框架仅仅在用于匹配URI的路由表中选择第一个路由。

这里有两个特殊的占位符:”{controller}“和“{action}”。

  • “{controller}“提供了控制器的名称。
  • “{action}“提供了动作的名称。在Web API中,通过会忽略“{action}”。

Defaults

如果你提供了默认的API,路由将会匹配缺少这些的URI。例如:

  1. routes.MapHttpRoute(
  2. name: "DefaultApi",
  3. routeTemplate: "api/{controller}/{category}",
  4. defaults: new { category = "all" }
  5. );

对于URI http: //localhost/api/products 将会匹配这个路由。{category} 字段会被分配默认值 all

路由字典(Route Dictionary)

如果框架发现了URI的一个匹配,它会创建一个包含了每个占位符适用的值的字典集合。键是不包含大括号的占位符名称。值是提取自URI路径或者默认表单。该字典被存储在IHttpRouteData对象中。

在路由匹配阶段,“{controller}“和”{action}“占位符会被像其他占位符一样对待。它们被同其他值一起简单地存储在字典中。

对于defaults,它可以有一个特殊值RouteParameter.Optional。如果一个占位符被分配到这个值,那么这个值不会被添加到路由字典中。例如:

  1. routes.MapHttpRoute(
  2. name: "DefaultApi",
  3. routeTemplate: "api/{controller}/{category}/{id}",
  4. defaults: new { category = "all", id = RouteParameter.Optional }
  5. );

对于URI路径“api/products”,路由字典将会包含:

  • controller:“products”
  • category:“all”

然而对于“api/products/toys/123”,路由字典将会包含:

  • controller:“products”
  • category:“all”
  • id:“123“

对于defaults,它同样也会包含一个没有在路由模板中任何地方出现的值。如果路由匹配了,这个值会被存储在字典中。例如:

  1. routes.MapHttpRoute(
  2. name: "Root",
  3. routeTemplate: "api/root/{id}",
  4. defaults: new { controller = "customers", id = RouteParameter.Optional }
  5. );

如果URI路径“api/root/8”,字典将会包含两个值:

  • controller:”customers“
  • id:“8”

选择控制器(Selecting a Controller)

控制器的选择由IHttpControllerSelector.SelectController方法来处理。这个方法需要传入一个HttpRequestMessage实例并返回HttpControllerDescriptor对象。默认的实现是由DefaultHttpControllerSelector类来实现的。这个类使用了一个简单的算法:

  • 在路由字典中查找键”controller“。
  • 提取出这个键对应的值,并添加字符串“Controller”以得到控制器的类型名
  • 用这个类型名来查找一个Web API控制器

例如,如果路由字典包含键值对“controller”=“products”,那么控制器类型就是“ProductsController”。如果这里不存在匹配的类型,或存在多个匹配,那么框架就会向客户端发送一个错误。

对于步骤3,DefaultHttpControllerSelector会使用IHttpControllerTypeResolver接口来得到Web API控制器类型的列表。IHttpControllerTypeResolver的默认实现会返回(a)实现IHttpController,(b)不是抽象的,(c)名称以“Controller“结尾的所有公共的类。

动作选择

在选择控制器之后,框架会通过调用IHttpActionSelector.SelectAction方法来选择动作。这个方法需要传入一个HttpControllerContext参数以及返回一个HttpActionDescriptor对象。

默认的实现由ApiControllerActionSelector类来提供。为了选择一个动作,它会按以下要求来查找: 1) 请求的HTTP方法 2) 路由模板中的“{action}“占位符(如果存在) 3) 控制器中动作的参数

在查看选择算法之前,我们需要理解关于控制器动作的一些东西。

控制器中的哪些方法会被认为是“动作“?当选择一个动作时,框架仅仅在控制器中查找公共的实例方法。当然了,它会排除一些”特殊“的方法(构造函数,事件,操作重载等等)和继承自ApiController类的方法。

HTTP方法。框架只会选择匹配请求的HTTP方法的动作,它取决于以下几点:

  • 你可以用某个属性来具体说明是HTTP方法:AcceptVerbs,HttpDelete,HttpGet,HttpHead,HttpOptions,HttpPatch,HttpPost或HttpPut。
  • 或者,如果一个控制器方法的名称以”Get”,“Post“,”Put“,”Delete“,”Head“,”Options“或”Patch“开始,那么按照约定该动作就支持HTTP方法。
  • 如果不包含以上几点,但支持POST的方法。

参数绑定。参数绑定是指Web API如何如何为参数创建一个值。这里是参数绑定的默认规则:

  • 简单类型直接从URI中提取
  • 复杂类型从请求体重提取

简单类型包括所有.NET框架基本类型(.NET Framework primitive types),再加上DateTime、Decimal、Guid、String和TimeSpan。对于每个动作,最多有一个参数可以读取请求体。

备注:重载默认绑定规则也是有可能的。查看WebAPI Parameter binding under the hood.

有了以上这些背景知识,这里是动作选择的算法:

  • 基于HTTP请求方法匹配到的控制器创建一个动作列表。
  • 动作路由字典包含“action“记录,移除其名字不匹配该值的动作。
  • 根据如下规则,尽力将动作参数匹配到URI:
  • a 对于每个动作,当绑定从URI中获得参数时得到一个简单类型的参数列表。执行可选的参数。
  • b 从这个列表中,无论是在路由字典中还是URI查询字符串中,都尽力找出针对每个参数名称的匹配。匹配不区分大小写并且不取决于参数顺序。
  • c 当列表中的每个参数在URI中都有一个匹配时,选择一个动作。
  • d 如果多个动作符合这些标准,那么选择其中一个有最多参数匹配的。
  • 忽略包含[NonAction]属性的动作。

步骤3可能是最容易迷惑的。基本的思想是参数可以从URI、请求体或绑定中获得它的值。对于来自URI的参数,我们会确保URI确实包含一个给参数的值,不论是在路径(通过路由字典)还是在查询字符串中。

例如,考虑如下动作:

  1. public void Get(int id)

这个id参数绑定到URI上,因此,这个动作可以匹配到包含一个给“id“的值的URI,不论是在路由字典还是查询字符串中。

可选参数是个例外,因为它们是可选的。对于可选参数,如果这个绑定不了从URI中得到这个值也是没关系的。

因为一些不同的原因,复杂类型也是个例外。复杂类型只能通过自定义绑定来绑定到URI上。但是在这种情况下,框架无法事先知道参数可能被绑定到一个特殊的URI。为了弄清楚它,就需要去执行这个绑定。这个选择算法的目标是在执行任何绑定之前,从静态描述中去选择一个动作。因此,复杂类型会从这种匹配算法中执行。

在动作被选取好了,所有的参数绑定也就被执行了。

总结:

  • 动作必须匹配请求的HTTP方法。
  • 动作名(如果存在)必须匹配路由字典中的“action“词条
  • 对于动作的所有参数,如果参数提取自URI,那么参数名必须在路由字典或URI查询字符串中被找到。(可选参数和复杂类型的参数除外。)
  • 尽量去匹配最多的参数数目。但最好的匹配也可能是不包含任何参数的方法。

扩展示例(Extended Example)

路由:

  1. routes.MapHttpRoute(
  2. name: "ApiRoot",
  3. routeTemplate: "api/root/{id}",
  4. defaults: new { controller = "products", id = RouteParameter.Optional }
  5. );
  6. routes.MapHttpRoute(
  7. name: "DefaultApi",
  8. routeTemplate: "api/{controller}/{id}",
  9. defaults: new { id = RouteParameter.Optional }
  10. );

控制器:

  1. public class ProductsController : ApiController
  2. {
  3. public IEnumerable<Product> GetAll() {}
  4. public Product GetById(int id, double version = 1.0) {}
  5. [HttpGet]
  6. public void FindProductsByName(string name) {}
  7. public void Post(Product value) {}
  8. public void Put(int id, Product value) {}
  9. }

HTTP请求:

  1. GET http://localhost:34701/api/products/1?version=1.5&details=1

路由匹配(Route Matching)

该URI会匹配到名为”DefaultApi”的路由。这个路由字典包含以下词条:

  • controller:“products”
  • id:“1“

这个路由字典不包含查询字符串“version”和“details”,但是在动作选择的时候这些仍然会被考虑。

控制器选择(Controller Selection)

根据路由字典中的“controller”词条,控制器类型是ProductsController。

动作选择(Action Selection)

该HTTP请求是一个GET请求。相应的支持GET的控制器动作是GetAll、GetById和FindProductsByName。路由字典中不包含任何“action“词条,所以我们不用去匹配动作名称。

接下来,我们尝试着匹配动作的参数名称,现在仅在GET动作中查找。

Action Parameters to Match
GetAll none
GetById "id"
FindProductsByName "name"

注意到GetById的version参数没有被考虑,因为它是一个可选参数。

显而易见GetAll方法能够匹配,GetById方法也能匹配,因为路由字典中包含“id“。FindProductsByName方法不匹配。

最后是GetById方法获胜,因为它能够匹配到一个参数,相对应的是没有参数能匹配GetAll。该方法伴随以下参数的值来执行:

  • id = 1
  • version = 1.5

注意到尽管version参数没有在选择算法中使用,但该参数的值也依旧是来自URI的查询字符串中。

扩展点(Extension Points)Web API为路由过程的一些部分提供了扩展点。

Interface Description
IHttpControllerSelector Selects the controller.
IHttpControllerTypeResolver Gets the list of controller types. The DefaultHttpControllerSelector chooses the controller type from this list.
IAssembliesResolver Gets the list of project assemblies. The IHttpControllerTypeResolverinterface uses this list to find the controller types.
IHttpControllerActivator Creates new controller instances.
IHttpActionSelector Selects the action.
IHttpActionInvoker Invokes the action.

为任何这些接口提供自己的实现,请使用HttpConfiguration对象上的Services集合:

  1. var config = GlobalConfiguration.Configuration;
  2. config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));

前言

路由是指Web API如何匹配到具体的动作。Web API 2支持一个新的路由类型,它被称为属性路由。正如其名,属性路由使用属性来定义路由。属性路由给予你在web API的URI上的更多控制。例如,你能轻易的创建用于描述层级资源的URI。

早期的路由风格被称为基于约定的路由,现在仍然被完整支持,你可以将这两种技术用于同一个项目中。

本主题演示如何启用属性的路由,并描述属性路由的各种选项。关于使用属性路由的实战教程,请查看Create a REST API with Attribute Routing in Web API 2。

• Why Attribute Routing? • Enabling Attribute Routing • Adding Route Attributes • Route Prefixes • Route Constraints • Optional URI Parameters and Default Values • Route Names • Route Order

前提条件(Prerequisites)

Visual Studio 2013 或 Visual Studio Express 2013

或者,使用NuGet Package Manager来安装必要的包。在Visual Studio的Tools目录下,选择Library Package Manager,然后选择Package Manager Console。在Package Manager Console窗口输入以下命令:

  1. Install-Package Microsoft.AspNet.WebApi.WebHost

Why Attribute Routing?

Web API的首个发行版使用基于约定的路由。在那种路由中,你定义一个或多个路由模板,它们是一些基本的参数字符串。当框架收到一个请求时,它会将URI匹配到路由模板中。(关于基于约定的路由的更多信息,请查看Routing in ASP.NET Web API)

基于约定的路由的一个优势是模板是定义在单一地方的,并且路由规则会被应用到所有的控制器。不幸的是,基于约定的路由很难去支持一个在RESTful API中很常见的URI模式。例如,资源通常包含着子资源:客户包含着订单,电影包含着演员,书籍包含着作者等等。所以很自然地创建映射这些关系的URI: /customers/1/orders 有了属性路由,就可以很轻易地定义一个针对该URI的路由。你只需要简单的添加一个属性到控制器动作上:

  1. [Route("customers/{customerId}/orders")]
  2. public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

这里还有些因为有了属性路由而变得更加容易的其他模式:

API versioning

在本例中,”api/v1/products”相对于”api/v2/products”可能会路由到不同的控制器。 /api/v1/products /api/v2/products

Overloaded URI segments

在本例中,”1”是个订单数字,但是“pending”映射到一个集合。 /orders/1 /orders/pending

Multiple parameter types

在本例中,“1”是个订单数字,但是“2013/06/10”却是个日期。 /orders/1 /orders/2013/06/10

启用属性路由(Enabling Attribute Routing)

为了启用属性路由,需要在配置时调用MapHttpAttributeRoutes。这个扩展方法被定义在System.Web.Http.HttpConfigurationExtensions类中。

  1. using System.Web.Http;
  2.  
  3. namespace WebApplication
  4. {
  5. public static class WebApiConfig
  6. {
  7. public static void Register(HttpConfiguration config)
  8. {
  9. // Web API routes
  10. config.MapHttpAttributeRoutes();
  11.  
  12. // Other Web API configuration not shown.
  13. }
  14. }
  15. }

属性路由也可以和基于约定的路由结合起来。为了定义基于约定的路由,调用MapHttpRoute方法。

  1. public static class WebApiConfig
  2. {
  3. public static void Register(HttpConfiguration config)
  4. {
  5. // Attribute routing.
  6. config.MapHttpAttributeRoutes();
  7.  
  8. // Convention-based routing.
  9. config.Routes.MapHttpRoute(
  10. name: "DefaultApi",
  11. routeTemplate: "api/{controller}/{id}",
  12. defaults: new { id = RouteParameter.Optional }
  13. );
  14. }
  15. }

关于配置Web API的更多信息,请查看Configuring ASP.NET Web API 2。

在Web API 2之前,Web API项目目标生成的代码像是这样:

  1. protected void Application_Start()
  2. {
  3. // WARNING - Not compatible with attribute routing.
  4. WebApiConfig.Register(GlobalConfiguration.Configuration);
  5. }

如果属性路由没有被启用,这个代码将会抛出异常。如果你升级一个已有的Web API项目来使用属性路由,请确保像下面这样升级了配置代码:

  1. protected void Application_Start()
  2. {
  3. // Pass a delegate to the Configure method.
  4. GlobalConfiguration.Configure(WebApiConfig.Register);
  5. }

备注:关于更多信息,请查看Configuring Web API with ASP.NET Hosting

添加路由属性(Adding Route Attributes)

这里是一个使用属性定义路由的示例:

  1. public class OrdersController : ApiController
  2. {
  3. [Route("customers/{customerId}/orders")]
  4. [HttpGet]
  5. public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
  6. }

字符串“customers/{customerId}/orders”是一个用于路由的URI模板。Web API会尽力将请求的URI匹配到模板中。在本例中,”customers“和”orders“都是字面字段,而”{customerId}”是变量参数。以下这些URI会匹配这个模板:

  1. 1 http://localhost/customers/1/orders
  2. 2 http://localhost/customers/bob/orders
  3. 3 http://localhost/customer/1234-5678/orders

你能够使用约束来限制这些匹配,这将会在本主题的后面进行介绍。

注意到路由模板“{customerId}”参数匹配到方法中的customerId参数名。当Web API执行控制器动作时,它会尽力绑定路由参数。例如,当URI是 http: //example.com/customers/1/orders 时,Web API会尽力将值”1“和动作中的customerId参数进行绑定。

一个URI模板可以有多个参数:

  1. [Route("customers/{customerId}/orders/{orderId}")]
  2. public Order GetOrderByCustomer(int customerId, int orderId) { ... }

任何没有路由属性的控制器方法都使用基于约定的路由。在此基础上,你能够在同一个项目中同时使用这两种路由类型。

HTTP Methods

Web API也会基于HTTP方法的请求(GET、POST等)来选择动作。默认地,Web API会根据控制器方法名且不区分大小写地查找匹配。例如,一个控制器方法名为PutCustomers,它匹配一个HTTP的PUT请求。

你也可以通过给方法加上这些属性来重载这个规则:

• [HttpDelete] • [HttpGet] • [HttpHead] • [HttpOptions] • [HttpPatch] • [HttpPost] • [HttpPut]

下面的例子映射CreateBook方法到HTTP的POST请求。

  1. [Route("api/books")]
  2. [HttpPost]
  3. public HttpResponseMessage CreateBook(Book book) { ... }

对于所有的HTTP方法,包括非标准方法,可以使用AcceptVerbs属性,它需要传入一个HTTP方法的列表。

  1. // WebDAV method
  2. [Route("api/books")]
  3. [AcceptVerbs("MKCOL")]
  4. public void MakeCollection() { }

路由前缀(Route Prefixes)

通常,控制器中的路由都以同样的前缀开始。例如:

  1. public class BooksController : ApiController
  2. {
  3. [Route("api/books")]
  4. public IEnumerable<Book> GetBooks() { ... }
  5.  
  6. [Route("api/books/{id:int}")]
  7. public Book GetBook(int id) { ... }
  8.  
  9. [Route("api/books")]
  10. [HttpPost]
  11. public HttpResponseMessage CreateBook(Book book) { ... }
  12. }

你可以通过使用[RoutePrefix]属性来为整个控制器设置一个公共前缀。

  1. [RoutePrefix("api/books")]
  2. public class BooksController : ApiController
  3. {
  4. // GET api/books
  5. [Route("")]
  6. public IEnumerable<Book> Get() { ... }
  7.  
  8. // GET api/books/5
  9. [Route("{id:int}")]
  10. public Book Get(int id) { ... }
  11.  
  12. // POST api/books
  13. [Route("")]
  14. public HttpResponseMessage Post(Book book) { ... }
  15. }

使用在方法属性上使用一个通配符(~)来重载路由前缀。

  1. [RoutePrefix("api/books")]
  2. public class BooksController : ApiController
  3. {
  4. // GET /api/authors/1/books
  5. [Route("~/api/authors/{authorId:int}/books")]
  6. public IEnumerable<Book> GetByAuthor(int authorId) { ... }
  7.  
  8. // ...
  9. }

路由前缀也可以包含参数:

  1. [RoutePrefix("customers/{customerId}")]
  2. public class OrdersController : ApiController
  3. {
  4. // GET customers/1/orders
  5. [Route("orders")]
  6. public IEnumerable<Order> Get(int customerId) { ... }
  7. }

路由约束(Route Constraints)

路由约束能够让你限制路由模板中的参数如何被匹配。大体的语法是“{parameter:constraint}”。例如:

  1. [Route("users/{id:int}"]
  2. public User GetUserById(int id) { ... }
  3.  
  4. [Route("users/{name}"]
  5. public User GetUserByName(string name) { ... }

在这里,第一个路由只有当URI的“id”字段是整型时才会被选择。否则将会选择第二个路由。

下表列出了被支持的约束。

Constraint Description Example
alpha Matches uppercase or lowercase Latin alphabet characters (a-z, A-Z) {x:alpha}
bool Matches a Boolean value. {x:bool}
datetime Matches a DateTime value. {x:datetime}
decimal Matches a decimal value. {x:decimal}
double Matches a 64-bit floating-point value. {x:double}
float Matches a 32-bit floating-point value. {x:float}
guid Matches a GUID value. {x:guid}
int Matches a 32-bit integer value. {x:int}
length Matches a string with the specified length or within a specified range of lengths. {x:length(6)} {x:length(1,20)}
long Matches a 64-bit integer value. {x:long}
max Matches an integer with a maximum value. {x:max(10)}
maxlength Matches a string with a maximum length. {x:maxlength(10)}
min Matches an integer with a minimum value. {x:min(10)}
minlength Matches a string with a minimum length. {x:minlength(10)}
range Matches an integer within a range of values. {x:range(10,50)}
regex Matches a regular expression. {x:regex(^\d{3}-\d{3}-\d{4}$)}

注意到其中一些约束在括号内还需要参数,比如“min”。你可以应用多个约束到一个参数,通过冒号分隔。

  1. [Route("users/{id:int:min(1)}")]
  2. public User GetUserById(int id) { ... }

自定义路由约束(Custom Route Constraints)你可以通过实现IHttpRouteConstraint接口来创建一个自定义路由约束。例如,以下约束限制了一个参数到非零整型值。

  1. public class NonZeroConstraint : IHttpRouteConstraint
  2. {
  3. public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
  4. IDictionary<string, object> values, HttpRouteDirection routeDirection)
  5. {
  6. object value;
  7. if (values.TryGetValue(parameterName, out value) && value != null)
  8. {
  9. long longValue;
  10. if (value is long)
  11. {
  12. longValue = (long)value;
  13. return longValue != 0;
  14. }
  15.  
  16. string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
  17. if (Int64.TryParse(valueString, NumberStyles.Integer,
  18. CultureInfo.InvariantCulture, out longValue))
  19. {
  20. return longValue != 0;
  21. }
  22. }
  23. return false;
  24. }
  25. }

下面的代码展示了如何去注册约束:

  1. public static class WebApiConfig
  2. {
  3. public static void Register(HttpConfiguration config)
  4. {
  5. var constraintResolver = new DefaultInlineConstraintResolver();
  6. constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));
  7.  
  8. config.MapHttpAttributeRoutes(constraintResolver);
  9. }
  10. }

现在你可以将该约束应用到你的路由中了:

  1. [Route("{id:nonzero}")]
  2. public HttpResponseMessage GetNonZero(int id) { ... }

你也可以通过实现IInlineConstraintResolver接口来替换整个DefaultInlineConstraintResolver类。这样做会替换掉所有的内建约束,除非你实现的IInlineConstraintResolver特意添加了它们。

可选的URI参数和默认值

你可以通过添加问好标记到路由参数让一个URI参数变成可选的。如果一个路由参数是可选的,你必须为方法参数定义默认值。

  1. public class BooksController : ApiController
  2. {
  3. [Route("api/books/locale/{lcid:int?}")]
  4. public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
  5. }

在本例中,/api/books/locale/1033和/api/books/locale会返回相同的资源。

或者,你可以特定一个默认值在路由模板中,如下所示:

  1. public class BooksController : ApiController
  2. {
  3. [Route("api/books/locale/{lcid:int=1033}")]
  4. public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
  5. }

这和前一个例子大体相同,但当默认值被应用时存在细微差别。 1, 在第一个例子(“{Icid?}”),默认值1033会被直接分配到方法参数,所以参数将会拥有一个准确的值。 2, 在第二个例子(“{Icid=1033}”),默认值1033会通过模型绑定过程。默认的模型绑定将会把1033转换成数字值1033。然而,你可以遇到一个自定义的模型绑定,而这可能会出错。 (多数情况下,除非你在你的管道中有自定义模型绑定,否则这两只表单形式是等价的。)

路由名称(Route Names)

在Web API中,每种路由都有一个名称。路由名称对于生成链接是非常有用的,正因此你才能在HTTP相应中包含一个链接。

为了指定路由名称,在属性上(attribute)设置Name属性(property)。以下示例展示了如何选择一个路由名称,以及当生成一个链接时如何使用路由名称。

  1. public class BooksController : ApiController
  2. {
  3. [Route("api/books/{id}", Name="GetBookById")]
  4. public BookDto GetBook(int id)
  5. {
  6. // Implementation not shown...
  7. }
  8.  
  9. [Route("api/books")]
  10. public HttpResponseMessage Post(Book book)
  11. {
  12. // Validate and add book to database (not shown)
  13.  
  14. var response = Request.CreateResponse(HttpStatusCode.Created);
  15.  
  16. // Generate a link to the new book and set the Location header in the response.
  17. string uri = Url.Link("GetBookById", new { id = book.BookId });
  18. response.Headers.Location = new Uri(uri);
  19. return response;
  20. }
  21. }

路由顺序(Route Order)

当框架试图用路由匹配URI时,它会得到一个特定的路由顺序。为了指定顺序,在路由属性上设置RouteOrder属性。小写的值在前,默认顺序值是零。

以下是如何确定所有的顺序的过程: 1. 比较每个路由属性的RouteOrder属性 2. 在路由模板上查找每个URI字段。对于每个字段,顺序由以下因素确定: - 字面字段 - 包含约束的路由参数 - 不包含约束的路由参数 - 包含约束的通配符参数字段 - 不包含约束的通配符参数字段 3. In the case of a tie,路由的顺序由路由模板的不区分大小写的原始字符串比较来确定。

这是一个示例。假定你定义如下控制器:

  1. [RoutePrefix("orders")]
  2. public class OrdersController : ApiController
  3. {
  4. [Route("{id:int}")] // constrained parameter
  5. public HttpResponseMessage Get(int id) { ... }
  6.  
  7. [Route("details")] // literal
  8. public HttpResponseMessage GetDetails() { ... }
  9.  
  10. [Route("pending", RouteOrder = 1)]
  11. public HttpResponseMessage GetPending() { ... }
  12.  
  13. [Route("{customerName}")] // unconstrained parameter
  14. public HttpResponseMessage GetByCustomer(string customerName) { ... }
  15.  
  16. [Route("{*date:datetime}")] // wildcard
  17. public HttpResponseMessage Get(DateTime date) { ... }
  18. }

这些路由的顺序如下:

  • orders/details
  • orders/{id}
  • orders/{customerName}
  • orders/{*date}
  • orders/pending

注意到“details”是一个字面字段,并且出现在“{id}”的前面,而“pending”出现在最后是因为它的RouteOrder是1。(这个例子假定不存在customer被命名为”details”和“pending”。通常来说,要尽量避免含糊不清的路由。在本例中,对于GetByCustomer的一个更好的路由模板是”customers/{customerName}”。)