路由(Routing)就是Web API如何将一个URI匹配到一个action的过程。Web API 2 支持一个新的路由方式-属性路由(attribute routing)。顾名思义,属性路由使用标识属性去定义路由,属性路由可使你在Web API中更方便的定制你的URIs。例如,你能够很容易的建立描述不一样层次资源的URIs。express
前面将的路由风格,叫作基于约定的路由(convention-based)在Web API中也彻底支持,实际上,你可以在项目中同时使用两种路由技术。api
这篇文章主要演示如何在项目中启用属性路由(attribute routing)和描述各类属性路由的使用方式,主要内容:框架
--一、为何须要属性路由ui
--二、容许属性路由spa
--三、添加路由属性版本控制
--四、路由前缀code
--五、路由约束blog
--六、可选择的URI参数以及默认值排序
--七、路由名称接口
--八、路由顺序
一、为何须要属性路由
第一个发布版的Web API使用 convention-based routing(基于约定的)。在这种风格的路由中,你定义一个或者多个路由模版,基本上是参数化的字符串。当框架接收到一个请求,框架将URI与路由模版进行匹配。
convention-based路由的一个优点是:路由模版定义在一个文件中,路由规则被应用到因此的控制器上。可是convention-based方式的路由风格,要实现支持像RESTful APIs中常见的特定的URI模式比较麻烦。例如,资源常常包含子资源:Customers have orders, movies have actors, books have authors,等等。建立可以反映这种关系的URI是必须的:/customers/1/orders
使用convention-based 路由很难去实现这种风格的URI(This type of URI is difficult to create using convention-based routing),尽管能够实现,但结果不佳,若是你有不少控制器和资源类型。
使用属性路由,你能够很容易的为这样的URI定义一个路由,只须要给一个控制器的action添加一个标识属性:
[Route("customers/{customerId}/orders")] public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
还有一些状况下使用属性路由将很是方便:
--API 版本(API versioning)
/api/v1/products
/api/v2/products
解释:假设要控制请求访问不一样版本的api,若是是convention-based风格的路由,意味着这里的v1 ,v2被定义为参数,那么必须在action中接收这个参数,而后在action中才能判断出版本(貌似这个时候知道版本用处不大了)咱们要实现的是v1 ,v2 访问的是不一样的控制器。那么使用属性路由很容易实现这一点,好比一个方法只在v2中有:
[Route("/api/v2/products")] public Ienumerable<Product> GetAll(){}
(如何实现版本控制,细节可能要在项目中去感觉)
--重载 URI片断(Overloaded URI segments)
/orders/1
/orders/pending
这个例子中"1"是一个订单编号,可是"pending"对应了一个订单集合。
--复杂的参数类型
/orders/1
/orders/2013/06/16
这个例子中"1" 是一个订单编号,可是"2013/06/16"指定了一个日期。
二、容许属性路由
Global.asax文件
protected void Application_Start() { // Pass a delegate to the Configure method. GlobalConfiguration.Configure(WebApiConfig.Register); }
WebApiConfig类
public static void Register(HttpConfiguration config) { // Web API routes config.MapHttpAttributeRoutes(); // Other Web API configuration not shown. }
config.MapHttpAttributeRoutes()方法启用属性路由。
三、添加路由标识属性
一个例子:
public class OrdersController : ApiController { [Route("customers/{customerId}/orders")] [HttpGet] public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... } }
字符串"customers/{customerId}/orders"是路由的URI模版,Web API尝试将请求URI与这个模版匹配,这个例子中"coustomers" 和 "orders" 是纯文本片断,{customerId}是占位符,下面的URI都会与这个模版匹配:
可使用约束限制{customerId}匹配的范围,下面会讲到。
注意在路由模版中的{customerId}参数,和action方法中的customerId参数匹配,当Web API调用控制器的action,将进行参数绑定,例如若是URI是: http://example.com/customers/1/orders
Web API会将值“1”传递的action方法的customerId参数。一个URI模版能够有多个占位符参数:
[Route("customers/{customerId}/orders/{orderId}")] public Order GetOrderByCustomer(int customerId, int orderId) { ... }
--HTTP请求方式
默认状况下Action方法使用方法开头用请求方式名称的方式两匹配不一样的HTTP请求(忽略大小写的),能够经过添加标识属性来指定某一个action方法匹配的HTTP请求方式:
[HttpDelete]
[HttpGet]
[HttpHead]
[HttpOptions]
[HttpPatch]
[HttpPost]
[HttpPut]
例如:
[Route("api/books")] [HttpPost] public HttpResponseMessage CreateBook(Book book) { ... }// WebDAV method [Route("api/books")] [AcceptVerbs("MKCOL")] public void MakeCollection() { }
四、路由前缀
不少时候一个控制器下的action路由模版的前面部分都是相同的,为了避免重复书写,能够这样:
[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")] 给控制器下的全部action方法增长了路由模版前缀。
若是有特殊状况,你能够在action方法的标识属性中使用浪符号(~)来覆盖指定的统一的路由前缀:
[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) { ... } }
五、路由约束
路由约束容许你限制路由模版中占位符参数的匹配范围,基本语法是{parameter:constraint}。例如:
[Route("users/{id:int}"] public User GetUserById(int id) { ... } [Route("users/{name}"] public User GetUserByName(string name) { ... }
GetUserById 方法只匹配id参数为整型的URI
支持的约束条件:
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)} |
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)}")] public User GetUserById(int id) { ... }
自定义路由约束
经过实现IHttpRouteConstraint接口来建立自定义的路由约束,例以下面的代码定义了一个“不能为0”的整数约束。
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); } }
如今就能够在路由中使用这个自定义的约束条件了:id必须为非0整数
[Route("{id:nonzero}")] public HttpResponseMessage GetNonZero(int id) { ... }
还能够经过实现IInlineConstraintResolver 接口的方式来覆盖全部的内置的约束。
六、可选择的URI参数和默认值
你能够经过给路由参数添加 问号"?”的方式来标识这个参数是可选的,若是路由模版中定义了可选参数,那么必须为action方法参数指定一个默认值(可选参数)。
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:int?}”,默认值直接在action方法的参数位置指定,因此action方法有一个肯定类型的默认值。
--第二个例子中“{lcid=1033}”,由于在路由模版中指定的默认值,那么须要通过模型绑定的过程,模型绑定过程会将“1033”从字符类型转换成数字类型,如何自定义了模型绑定方式,可能还有其余的不一样的地方。
两个例子的功能通常状况下都是相同的,除非自定义了模型绑定。
七、路由名称
在Web API中每个路由项都有一个名称,路由名称在生成连接的时候很是有用,隐藏你能够在返回消息中包含一个有效的连接。
使用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 属性,较小的值排在前面,默认的排序值是0。
排序是如何肯定的:
1.比较路由标识属性的RouteOrder属性值。
2.查看路由模版中的每个URI片断,对于每个片断,按照下面的方式排序
1-纯文本片断
2-带约束条件的路由参数
3-不带约束条件的路由参数
4-带约束条件的通配符路由参数
5不带约束条件的通配符路由参数
3.In the case of a tie, routes are ordered by a case-insensitive ordinal string comparison (OrdinalIgnoreCase) of the route template.
看例子:
[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
前面有讲过,在URI匹配路由模版时是从路由的排列顺序开始匹配,一旦匹配成功则会忽略后面的路由模版了。