- 2 路由
- 路由表
- 路由偏差(Routing Variations)
- 通过动作名进行路由
- 无动作(Non-Actions)
- 路由模板(Route Templates)
- Defaults
- 路由字典(Route Dictionary)
- 选择控制器(Selecting a Controller)
- 动作选择
- 扩展示例(Extended Example)
- 路由匹配(Route Matching)
- 控制器选择(Controller Selection)
- 动作选择(Action Selection)
- 扩展点(Extension Points)Web API为路由过程的一些部分提供了扩展点。
- 前言
- 前提条件(Prerequisites)
- Why Attribute Routing?
- 启用属性路由(Enabling Attribute Routing)
- 添加路由属性(Adding Route Attributes)
- HTTP Methods
- 路由前缀(Route Prefixes)
- 路由约束(Route Constraints)
- 自定义路由约束(Custom Route Constraints)你可以通过实现IHttpRouteConstraint接口来创建一个自定义路由约束。例如,以下约束限制了一个参数到非零整型值。
- 可选的URI参数和默认值
- 路由名称(Route Names)
- 路由顺序(Route Order)
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项目模板就创建了一个默认的路由表:
- routes.MapHttpRoute(
- name: "API Default",
- routeTemplate: "api/{controller}/{id}",
- defaults: new { id = RouteParameter.Optional }
- );
这个路由被定义在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”字段。
- /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},会被映射到动作的参数。
让我们来看一个示例。假定你定义了如下的控制器:
- public class ProductsController : ApiController
- {
- public void GetAllProducts() { }
- public IEnumerable<Product> GetProductById(int id) { }
- public HttpResponseMessage DeleteProduct(int id){ }
- }
这里是一些可能的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请求:
- public class ProductsController : ApiController
- {
- [HttpGet]
- public Product FindProduct(id) {}
- }
为了让一个动作支持多个HTTP方法,或支持除GET、PUT、POST和DELETE之外的HTTP方法,你可以使用AcceptVerbs属性,它以一个HTTP方法列表为参数。
- public class ProductsController : ApiController
- {
- [AcceptVerbs("GET", "HEAD")]
- public Product FindProduct(id) { }
- // WebDAV method
- [AcceptVerbs("MKCOL")]
- public void MakeCollection() { }
- }
通过动作名进行路由
有了默认的路由模板,Web API使用HTTP方法来选择动作。然而,你也可以创建一个将动作名包含在URI中的路由表。
- routes.MapHttpRoute(
- name: "ActionApi",
- routeTemplate: "api/{controller}/{action}/{id}",
- defaults: new { id = RouteParameter.Optional }
- );
在这个路由模板中,{action}参数在控制器中命名了一个动作方法。在这种风格的路由中,应使用属性来指定允许的HTTP方法。例如,假定你的控制器有了以下方法:
- public class ProductsController : ApiController
- {
- [HttpGet]
- public string Details(int id);
- }
在这种情况下,对于“api/products/details/1”的GET请求被被映射到Details方法。这种风格的路由和ASP.NET MVC很接近,并且可能适合于RPC风格的API。
你可以通过ActionName属性来重写动作名。在接下来的例子中,存在两个都映射到”api/products/thumbnail/id”的动作。其中一个支持GET,另一个支持POST:
- public class ProductsController : ApiController
- {
- [HttpGet]
- [ActionName("Thumbnail")]
- public HttpResponseMessage GetThumbnailImage(int id);
- [HttpPost]
- [ActionName("Thumbnail")]
- public void AddThumbnailImage(int id);
- }
无动作(Non-Actions)
为了阻止一个方法被当作动作来执行,可以使用NonAction属性。这会框架指明该方法并非一个动作,即使是它可能匹配到路由规则。
- // Not an action method.
- [NonAction]
- public string GetPrivateData() { ... }
这篇文章描述了ASP.NET Web API如何将HTTP请求路由到控制器上的特定动作。
备注:想要了解关于路由的高层次概述,请查看Routing in ASP.NET Web API。
这篇文章侧重于路由过程的细节。如果你创建了一个Web API项目并且发现一些请求并没有按你预期得到相应的路由,希望这篇文章有所帮助。
路由有以下三个主要阶段:
- 将URI匹配到路由模板
- 选择一个控制器
- 选择一个动作
你可以用自己的习惯行为来替换其中一些过程。在本文中,我会描述默认行为。在结尾,我会指出你可以自定义行为的地方。
路由模板(Route Templates)
路由模板看起来和URI路径非常相似,但是它能包含用大括号指明的占位符。
- "api/{controller}/public/{category}/{id}"
当你创建了一个路由,你为一些或全部占位符提供默认的值:
- defaults: new { category = "all" }
你也可以提供一些约束(constraints),它限制了URI字段如何才能匹配一个占位符:
- 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。例如:
- routes.MapHttpRoute(
- name: "DefaultApi",
- routeTemplate: "api/{controller}/{category}",
- defaults: new { category = "all" }
- );
对于URI http: //localhost/api/products 将会匹配这个路由。{category} 字段会被分配默认值 all。
路由字典(Route Dictionary)
如果框架发现了URI的一个匹配,它会创建一个包含了每个占位符适用的值的字典集合。键是不包含大括号的占位符名称。值是提取自URI路径或者默认表单。该字典被存储在IHttpRouteData对象中。
在路由匹配阶段,“{controller}“和”{action}“占位符会被像其他占位符一样对待。它们被同其他值一起简单地存储在字典中。
对于defaults,它可以有一个特殊值RouteParameter.Optional。如果一个占位符被分配到这个值,那么这个值不会被添加到路由字典中。例如:
- routes.MapHttpRoute(
- name: "DefaultApi",
- routeTemplate: "api/{controller}/{category}/{id}",
- defaults: new { category = "all", id = RouteParameter.Optional }
- );
对于URI路径“api/products”,路由字典将会包含:
- controller:“products”
- category:“all”
然而对于“api/products/toys/123”,路由字典将会包含:
- controller:“products”
- category:“all”
- id:“123“
对于defaults,它同样也会包含一个没有在路由模板中任何地方出现的值。如果路由匹配了,这个值会被存储在字典中。例如:
- routes.MapHttpRoute(
- name: "Root",
- routeTemplate: "api/root/{id}",
- defaults: new { controller = "customers", id = RouteParameter.Optional }
- );
如果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确实包含一个给参数的值,不论是在路径(通过路由字典)还是在查询字符串中。
例如,考虑如下动作:
- public void Get(int id)
这个id参数绑定到URI上,因此,这个动作可以匹配到包含一个给“id“的值的URI,不论是在路由字典还是查询字符串中。
可选参数是个例外,因为它们是可选的。对于可选参数,如果这个绑定不了从URI中得到这个值也是没关系的。
因为一些不同的原因,复杂类型也是个例外。复杂类型只能通过自定义绑定来绑定到URI上。但是在这种情况下,框架无法事先知道参数可能被绑定到一个特殊的URI。为了弄清楚它,就需要去执行这个绑定。这个选择算法的目标是在执行任何绑定之前,从静态描述中去选择一个动作。因此,复杂类型会从这种匹配算法中执行。
在动作被选取好了,所有的参数绑定也就被执行了。
总结:
- 动作必须匹配请求的HTTP方法。
- 动作名(如果存在)必须匹配路由字典中的“action“词条
- 对于动作的所有参数,如果参数提取自URI,那么参数名必须在路由字典或URI查询字符串中被找到。(可选参数和复杂类型的参数除外。)
- 尽量去匹配最多的参数数目。但最好的匹配也可能是不包含任何参数的方法。
扩展示例(Extended Example)
路由:
- routes.MapHttpRoute(
- name: "ApiRoot",
- routeTemplate: "api/root/{id}",
- defaults: new { controller = "products", id = RouteParameter.Optional }
- );
- routes.MapHttpRoute(
- name: "DefaultApi",
- routeTemplate: "api/{controller}/{id}",
- defaults: new { id = RouteParameter.Optional }
- );
控制器:
- public class ProductsController : ApiController
- {
- public IEnumerable<Product> GetAll() {}
- public Product GetById(int id, double version = 1.0) {}
- [HttpGet]
- public void FindProductsByName(string name) {}
- public void Post(Product value) {}
- public void Put(int id, Product value) {}
- }
HTTP请求:
- 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集合:
- var config = GlobalConfiguration.Configuration;
- 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窗口输入以下命令:
- 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的路由。你只需要简单的添加一个属性到控制器动作上:
- [Route("customers/{customerId}/orders")]
- 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类中。
- using System.Web.Http;
- namespace WebApplication
- {
- public static class WebApiConfig
- {
- public static void Register(HttpConfiguration config)
- {
- // Web API routes
- config.MapHttpAttributeRoutes();
- // Other Web API configuration not shown.
- }
- }
- }
属性路由也可以和基于约定的路由结合起来。为了定义基于约定的路由,调用MapHttpRoute方法。
- public static class WebApiConfig
- {
- public static void Register(HttpConfiguration config)
- {
- // Attribute routing.
- config.MapHttpAttributeRoutes();
- // Convention-based routing.
- config.Routes.MapHttpRoute(
- name: "DefaultApi",
- routeTemplate: "api/{controller}/{id}",
- defaults: new { id = RouteParameter.Optional }
- );
- }
- }
关于配置Web API的更多信息,请查看Configuring ASP.NET Web API 2。
在Web API 2之前,Web API项目目标生成的代码像是这样:
- protected void Application_Start()
- {
- // WARNING - Not compatible with attribute routing.
- WebApiConfig.Register(GlobalConfiguration.Configuration);
- }
如果属性路由没有被启用,这个代码将会抛出异常。如果你升级一个已有的Web API项目来使用属性路由,请确保像下面这样升级了配置代码:
- protected void Application_Start()
- {
- // Pass a delegate to the Configure method.
- GlobalConfiguration.Configure(WebApiConfig.Register);
- }
备注:关于更多信息,请查看Configuring Web API with ASP.NET Hosting
添加路由属性(Adding Route Attributes)
这里是一个使用属性定义路由的示例:
- public class OrdersController : ApiController
- {
- [Route("customers/{customerId}/orders")]
- [HttpGet]
- public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
- }
字符串“customers/{customerId}/orders”是一个用于路由的URI模板。Web API会尽力将请求的URI匹配到模板中。在本例中,”customers“和”orders“都是字面字段,而”{customerId}”是变量参数。以下这些URI会匹配这个模板:
- 1, http://localhost/customers/1/orders
- 2, http://localhost/customers/bob/orders
- 3, http://localhost/customer/1234-5678/orders
你能够使用约束来限制这些匹配,这将会在本主题的后面进行介绍。
注意到路由模板“{customerId}”参数匹配到方法中的customerId参数名。当Web API执行控制器动作时,它会尽力绑定路由参数。例如,当URI是 http: //example.com/customers/1/orders 时,Web API会尽力将值”1“和动作中的customerId参数进行绑定。
一个URI模板可以有多个参数:
- [Route("customers/{customerId}/orders/{orderId}")]
- 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请求。
- [Route("api/books")]
- [HttpPost]
- public HttpResponseMessage CreateBook(Book book) { ... }
对于所有的HTTP方法,包括非标准方法,可以使用AcceptVerbs属性,它需要传入一个HTTP方法的列表。
- // WebDAV method
- [Route("api/books")]
- [AcceptVerbs("MKCOL")]
- public void MakeCollection() { }
路由前缀(Route Prefixes)
通常,控制器中的路由都以同样的前缀开始。例如:
- public class BooksController : ApiController
- {
- [Route("api/books")]
- public IEnumerable<Book> GetBooks() { ... }
- [Route("api/books/{id:int}")]
- public Book GetBook(int id) { ... }
- [Route("api/books")]
- [HttpPost]
- public HttpResponseMessage CreateBook(Book book) { ... }
- }
你可以通过使用[RoutePrefix]属性来为整个控制器设置一个公共前缀。
- [RoutePrefix("api/books")]
- public class BooksController : ApiController
- {
- // GET api/books
- [Route("")]
- public IEnumerable<Book> Get() { ... }
- // GET api/books/5
- [Route("{id:int}")]
- public Book Get(int id) { ... }
- // POST api/books
- [Route("")]
- public HttpResponseMessage Post(Book book) { ... }
- }
使用在方法属性上使用一个通配符(~)来重载路由前缀。
- [RoutePrefix("api/books")]
- public class BooksController : ApiController
- {
- // GET /api/authors/1/books
- [Route("~/api/authors/{authorId:int}/books")]
- public IEnumerable<Book> GetByAuthor(int authorId) { ... }
- // ...
- }
路由前缀也可以包含参数:
- [RoutePrefix("customers/{customerId}")]
- public class OrdersController : ApiController
- {
- // GET customers/1/orders
- [Route("orders")]
- public IEnumerable<Order> Get(int customerId) { ... }
- }
路由约束(Route Constraints)
路由约束能够让你限制路由模板中的参数如何被匹配。大体的语法是“{parameter:constraint}”。例如:
- [Route("users/{id:int}"]
- public User GetUserById(int id) { ... }
- [Route("users/{name}"]
- 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”。你可以应用多个约束到一个参数,通过冒号分隔。
- [Route("users/{id:int:min(1)}")]
- public User GetUserById(int id) { ... }
自定义路由约束(Custom Route Constraints)你可以通过实现IHttpRouteConstraint接口来创建一个自定义路由约束。例如,以下约束限制了一个参数到非零整型值。
- public class NonZeroConstraint : IHttpRouteConstraint
- {
- public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
- IDictionary<string, object> values, HttpRouteDirection routeDirection)
- {
- object value;
- if (values.TryGetValue(parameterName, out value) && value != null)
- {
- long longValue;
- if (value is long)
- {
- longValue = (long)value;
- return longValue != 0;
- }
- string valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
- if (Int64.TryParse(valueString, NumberStyles.Integer,
- CultureInfo.InvariantCulture, out longValue))
- {
- return longValue != 0;
- }
- }
- return false;
- }
- }
下面的代码展示了如何去注册约束:
- public static class WebApiConfig
- {
- public static void Register(HttpConfiguration config)
- {
- var constraintResolver = new DefaultInlineConstraintResolver();
- constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));
- config.MapHttpAttributeRoutes(constraintResolver);
- }
- }
现在你可以将该约束应用到你的路由中了:
- [Route("{id:nonzero}")]
- public HttpResponseMessage GetNonZero(int id) { ... }
你也可以通过实现IInlineConstraintResolver接口来替换整个DefaultInlineConstraintResolver类。这样做会替换掉所有的内建约束,除非你实现的IInlineConstraintResolver特意添加了它们。
可选的URI参数和默认值
你可以通过添加问好标记到路由参数让一个URI参数变成可选的。如果一个路由参数是可选的,你必须为方法参数定义默认值。
- public class BooksController : ApiController
- {
- [Route("api/books/locale/{lcid:int?}")]
- public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
- }
在本例中,/api/books/locale/1033和/api/books/locale会返回相同的资源。
或者,你可以特定一个默认值在路由模板中,如下所示:
- public class BooksController : ApiController
- {
- [Route("api/books/locale/{lcid:int=1033}")]
- public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
- }
这和前一个例子大体相同,但当默认值被应用时存在细微差别。 1, 在第一个例子(“{Icid?}”),默认值1033会被直接分配到方法参数,所以参数将会拥有一个准确的值。 2, 在第二个例子(“{Icid=1033}”),默认值1033会通过模型绑定过程。默认的模型绑定将会把1033转换成数字值1033。然而,你可以遇到一个自定义的模型绑定,而这可能会出错。 (多数情况下,除非你在你的管道中有自定义模型绑定,否则这两只表单形式是等价的。)
路由名称(Route Names)
在Web API中,每种路由都有一个名称。路由名称对于生成链接是非常有用的,正因此你才能在HTTP相应中包含一个链接。
为了指定路由名称,在属性上(attribute)设置Name属性(property)。以下示例展示了如何选择一个路由名称,以及当生成一个链接时如何使用路由名称。
- public class BooksController : ApiController
- {
- [Route("api/books/{id}", Name="GetBookById")]
- public BookDto GetBook(int id)
- {
- // Implementation not shown...
- }
- [Route("api/books")]
- public HttpResponseMessage Post(Book book)
- {
- // Validate and add book to database (not shown)
- var response = Request.CreateResponse(HttpStatusCode.Created);
- // Generate a link to the new book and set the Location header in the response.
- string uri = Url.Link("GetBookById", new { id = book.BookId });
- response.Headers.Location = new Uri(uri);
- return response;
- }
- }
路由顺序(Route Order)
当框架试图用路由匹配URI时,它会得到一个特定的路由顺序。为了指定顺序,在路由属性上设置RouteOrder属性。小写的值在前,默认顺序值是零。
以下是如何确定所有的顺序的过程: 1. 比较每个路由属性的RouteOrder属性 2. 在路由模板上查找每个URI字段。对于每个字段,顺序由以下因素确定: - 字面字段 - 包含约束的路由参数 - 不包含约束的路由参数 - 包含约束的通配符参数字段 - 不包含约束的通配符参数字段 3. In the case of a tie,路由的顺序由路由模板的不区分大小写的原始字符串比较来确定。
这是一个示例。假定你定义如下控制器:
- [RoutePrefix("orders")]
- public class OrdersController : ApiController
- {
- [Route("{id:int}")] // constrained parameter
- public HttpResponseMessage Get(int id) { ... }
- [Route("details")] // literal
- public HttpResponseMessage GetDetails() { ... }
- [Route("pending", RouteOrder = 1)]
- public HttpResponseMessage GetPending() { ... }
- [Route("{customerName}")] // unconstrained parameter
- public HttpResponseMessage GetByCustomer(string customerName) { ... }
- [Route("{*date:datetime}")] // wildcard
- public HttpResponseMessage Get(DateTime date) { ... }
- }
这些路由的顺序如下:
- orders/details
- orders/{id}
- orders/{customerName}
- orders/{*date}
- orders/pending
注意到“details”是一个字面字段,并且出现在“{id}”的前面,而“pending”出现在最后是因为它的RouteOrder是1。(这个例子假定不存在customer被命名为”details”和“pending”。通常来说,要尽量避免含糊不清的路由。在本例中,对于GetByCustomer的一个更好的路由模板是”customers/{customerName}”。)