2.3属性在 ASP.NET Web API 2 路由
路由是 Web API 如何匹配 URI 的行动。Web API 2 支持一种新型的路由,称为属性路由。顾名思义,属性路由使用属性来定义路由。属性路由给你更多的控制 Uri 在您的 web API。例如,您可以轻松创建描述层次结构的资源的 Uri。
早些时候的风格的路由,称为基于公约的路由,仍然完全支持。事实上,你可以结合这两种技术在同一个项目。
本主题演示如何启用属性的路由,并描述属性的路由的各种选项。使用属性路由端到端教程,请参阅创建属性路由 Web API 2 中的 REST API.
系统必备组件
或者,使用 NuGet 程序包管理器来安装所需的包。从 Visual Studio 中的工具菜单,选择库软件包管理器,然后选择程序包管理器控制台。在程序包管理器控制台窗口中输入以下命令 ︰
Install-Package Microsoft.AspNet.WebApi.WebHost
为什么属性路由?
Web API 的第一个版本使用基于公约的路由。该类型的路由,您定义一个或更多的路线模板,基本上都是参数化字符串。当框架收到请求时,它匹配的 URI 对工艺路线模板。(有关公约基于路由的详细信息,请参阅路由选择在 ASP.NET Web API.
公约基于路由的一个优点是在一个地方,定义模板和路由规则所有的控制器的应用保持一致。不幸的是,公约 》 基于路由难以支持某些常见 RESTful Api 中的 URI 模式。例如,资源通常包含子资源 ︰ 客户下了订单,电影有演员,书有作者,等等。很自然地创建反映这些关系的 Uri:
/customers/1/orders
这种类型的 URI 很难创建使用基于公约的路由。虽然它是可以完成的结果不能很好如果你有很多控制器或资源类型。
使用属性的路由,这是小事来此 uri 定义路由。你只需将属性添加到控制器操作 ︰
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
这里有一些其他属性路由可以方便的模式。
API 版本控制
在此示例中,"/ 原料药,v1,产品"将被路由到不同的控制器比"/ 原料药,v2,产品"。
/api/v1/products
/api/v2/products
重载的 URI 片段
在此示例中,"1"是订单编号,但是"挂起"映射到集合。
/orders/1
/orders/pending
多个参数类型
在此示例中,"1"是订单编号,但是"2013年/06/16"指定一个日期。
/orders/1
/orders/2013/06/16
启用属性路由选择
若要启用路由属性,请在配置过程中调用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 的详细信息,请参阅配置 ASP.NET Web API 2.
注 ︰ 从 Web API 1 迁移
在 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);
}
更多的信息,请参阅配置 Web API 与 ASP.NET 托管.
添加路由属性
这里是一个示例使用属性定义一条路线 ︰
public class OrdersController : ApiController
{
[Route("customers/{customerId}/orders")]
[HttpGet]
public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}
字符串"客户 / {客户 id} / 订单"是用于路由的 URI 模板。Web API 会尝试匹配请求的 URI 模板。在此示例中,"客户"和"订单"是文字片段,和客户 id"{}"是一个可变的参数。以下 Uri 将匹配此模板 ︰
- http://localhost/customers/1/orders
- http://localhost/customers/bob/orders
- http://localhost/customers/1234-5678/orders
您可以限制通过使用约束,稍后在本主题中描述的匹配。
工艺路线模板中的客户 id"{}"参数匹配的方法中的客户 id参数名称的通知。当 Web API 调用控制器操作时,它试图将绑定路由参数。例如,如果 URI 是http://example.com/customers/1/orders
,Web API 试图绑定到行动中的客户 id参数值"1"。
一个 URI 模板可以有几个参数 ︰
[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }
没有路由属性的任何控制器方法使用基于公约的路由。这种方式,你可以结合这两种类型的路由在同一个项目。
HTTP 方法
Web API 也选择基于 HTTP 方法 (GET、 邮政等) 请求的操作。默认情况下,Web API 查找开始时的控制器方法名称不区分大小写匹配。例如,一个名为PutCustomers
的控制器方法匹配 HTTP 请求。
您可以重写本公约的装饰方法与任何下列属性 ︰
- [] HttpDelete
- [公共]
- [] 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() { }
路由前缀
通常情况下,所有开始具有相同前缀的控制器中的路由。例如 ︰
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("users/{id:int}"]
public User GetUserById(int id) { ... }
[Route("users/{name}"]
public User GetUserByName(string name) { ... }
在这里,如果 URI 的"id"部分是一个整数,只将选中的第一个路由。否则,将选择第二条路线。
下表列出了支持的约束。
约束 | 描述 | 示例 |
---|---|---|
阿尔法 | 火柴大写或小写西文字母字符 (a-z、 A-Z) | {x ︰ 阿尔法} |
布尔值 | 匹配一个布尔值。 | {x:} bool |
日期时间 | 与DateTime值匹配。 | {x ︰ 日期时间} |
十进制 | 匹配一个十进制值。 | {x ︰ 十进制} |
双 | 匹配一个 64 位的浮点值。 | {x ︰ 双} |
浮法 | 与 32 位浮点值匹配。 | {x ︰ 浮} |
guid | 匹配一个 GUID 值。 | {x:} guid |
int | 匹配一个 32 位整数值。 | {x:} int |
长度 | 匹配字符串与指定的长度或在一个指定的长度范围内。 | {x:} length(6) {x:} length(1,20) |
长 | 匹配一个 64 位整数值。 | {x ︰ 长} |
最大 | 匹配,最大值的整数。 | {x:} max(10) |
maxlength | 匹配字符串最大长度。 | {x:} maxlength(10) |
分钟 | 匹配的最小值的整数。 | {x:} min(10) |
minlength | 匹配与最小长度的字符串。 | {x:} minlength(10) |
范围 | 匹配的值范围内的整数。 | {x:} range(10,50) |
正则表达式 | 匹配正则表达式。 | {x:} regex(^\d{3}-\d{3}-\d{4}$) |
注意到的一些限制,如"min",带参数在括号中。您可以将多个约束应用于参数,用冒号分隔。
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }
自定义路由约束
您可以通过实现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) { ... }
}
这是几乎相同的前面的示例,但略有不同的行为时应用的默认值。
- 在第一个示例 ("{lcid?}") 中,1033年的默认值直接分配给方法的参数,所以该参数会有此确切的值。
- 在第二个示例 ("{lcid = 1033年}"),"1033"的默认值经过模型绑定过程。默认的模型联编程序将转换为"1033"1033年的数字值。然而,您可以插入自定义模型联编程序,可能会做不同的事情。
(在大多数情况下,除非您有自定义模型联编程序在您的管道,这两种形式将等效。)
路由名称
在 Web API 中,每条路线有一个名称。路由名称可用于生成链接,这样你可以在 HTTP 响应中包含一个链接。
若要指定路由名称,在该属性上设置Name属性。下面的示例演示如何设置路由的名称,以及如何生成一个链接时,使用路由名称。
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;
}
}
工艺路线顺序
当框架试图匹配路由的 URI 时,它计算路线按特定的顺序。以指定的顺序,请将RouteOrder属性设置的路由属性上。首先计算较低的值。默认顺序值为零。
这里是如何确定的总排序 ︰
- 比较路由特性的RouteOrder属性。
- 看看每个 URI 段路线模板中。每个线段,次序如下 ︰
- 文字部分。
- 带约束的路由参数。
- 无约束的路由参数。
- 带有约束的通配符参数分段。
- 通配符参数部分不受限制。
- 在一条领带,路线按路线模板不区分大小写的序号字符串比较 (OrdinalIgnoreCase) 进行排序。
这里是一个例子。假设您要定义以下控制器 ︰
[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) { ... }
}
这些路线的顺序如下。
- 订单/详细信息
- 订单 / {id}
- 订单 / {customerName}
- 订单 / {* 日期}
- 订单 / 悬而未决
请注意,"详细信息"是一个文字部分和出现之前"{id}",但是"待审"会出现最后因为RouteOrder属性是 1。(此示例假定那里没有客户命名"详细信息"或"挂起"。一般情况下,尽量避免含糊不清的路线。在此示例中, GetByCustomer
为更好的路线模板是"客户 / {customerName}")
这篇文章的初衷是在 2014 年 1 月 20 日