前言
閱讀本文之前,您也可以到Asp.Net Web API 2 系列導航進行查看 http://www.cnblogs.com/aehyok/p/3446289.html
路由就是Web API如何把URI匹配到一個Action的描述。Web API支持一種新的路由類型,被叫做屬性路由。顧名思義,屬性路由是用屬性來創建路由。在你的Web API中屬性路由可以讓你更好的控制URI。你能容易的創建描述資源階層的URIs。
較早的基於公約的路由風格是全面被支持的。事實上,你能夠在同一個項目中聯合使用這兩種技術。
本文主要展示如何啟用屬性路由,並且描述了屬性路由的各種選項,內容如下:
1、為什么使用屬性路由?
2、啟用屬性路由
3、添加路由屬性
4、路由前綴
5、路由約束
6、可選的URI參數和默認值
7、路由名稱
8、路由順序
1、為什么使用屬性路由
第一個Web API版本使用的是基於公約的路由。在該類型的路由中, 你可以定義一個或者多個被參數化字符串的模版。當這個框架接收到一個請求時,它匹配一個URI到路由模版。有關基於公約的路由的詳細介紹可以參考之前的文章:http://www.cnblogs.com/aehyok/p/3442051.html
基於公約的路由的一個優勢就是,這個模版被定義在一個單獨的地方。這個路由規則一致的被應用於所有的控制器。不幸的是,基於公約的路由是很難支持確切的URI模式,而這個確切的URI模式在Restful APIs中是很普遍的。例如,資源經常包含子資源:客戶下了訂單,電影有演員,書有作者等等,它是很自然的創建這些URI來反應這些關系:
/customers/1/orders
這種類型的URI在基於公約的路由下是比較難實現的。盡管它能做到,但是如果你有許多控制器或者很多資源類型這種結果不能很好的被擴展。
對於屬性路由,它是很容易的為這個URI定義一個路由。你可以簡單的添加一個屬性到控制器的動作上:
[Route("customers/{customerId}/orders")] public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
這有一些屬性路由使它更容易的其他URI模式。
API版本控制
在下面的例子中,"api/v1/products"相對於"api/v2/products"將被路由到不同的控制器。
/api/v1/products
/api/v2/products
重載URI片段
在下面的例子中,"1"是一個階數,而"pending"被映射到集合。
/orders/1
/orders/pending
多個參數類型
在下面的例子中,"1"是一個階數,而“2013/06/16”被指定為一個日期。
/orders/1 /orders/2013/06/16
2、啟用屬性路由
要啟用屬性路由,在配置期間需要調用MapHttpAttributeRoutes。這個擴展方法被定義在System.Web.Http.HttpConfigurationExtensions類中。
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); } }
你也可以將屬性路由與基於公約的路由一起使用。為了定義基於公約的路由,需要調用MapHttpRoute 的方法。
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } }
3、添加路由屬性
下面是一個使用屬性定義的路由示例:
public class OrdersController : ApiController { [Route("customers/{customerId}/orders")] public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... } }
這個[Route]屬性定義了一個HTTP Get方法。這個字符串“customers/{customerId}/orders”是路由的URI模版。在路由模版中的“{customerId}”參數匹配了在方法中的customerId參數的名稱。 例如,這個路由將匹配如下的URI:
http://example.com/customers/1/orders
這個URI模版可以有多個參數:
[Route("customers/{customerId}/orders/{orderId}")] public Order GetOrderByCustomer(int customerId, int orderId) { ... }
任何沒有路由屬性的控制器方法將使用基於公約的路由。這種方式,你可以結合兩種方式在同一個項目中。
4、路由前綴
通常情況下,在同一個控制器中的所有路由以相同的前綴開頭。例如:
public class BooksController : ApiController { [Route("api/books")] public IEnumerable<Book> GetBooks() { ... } [Route("api/books/{id:int}")] public Book GetBook(int id) { ... } [Route("api/books")] 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) { ... } }
5、路由約束
路由約束可以讓你在路由模版中限制參數被匹配。常規的語法是"{parameter:constraint}",例如:
[Route("users/{id:int}"] public User GetUserById(int id) { ... } [Route("users/{name}"] public User GetUserByName(string name) { ... }
如果URI的“id”片段是一個integer類型的,那么第一個路由將會被選擇,否則第二個路由將會被選擇。
下面是被支持的約束列表:
注意到一些限制,例如"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) { ... }
你也能替換整個DefaultInlineConstraintResolver 類通過實現IInlineConstraintResolver 接口。這樣做將替換所有的內置約束。除非在IInlineConstraintResolver 的實現特地的添加它們。
6、可選的URI參數和默認值
你可以通過添加一個問號標記路由參數使成為一個可選的URI參數。如果一個路由參數是可選的,你必須為這個方法參數定義一個默認值。
public class BooksController : ApiController { [Route("api/books/locale/{lcid?}")] public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... } }
在這個例子中,"/api/books/locale/1033"和"
/api/books/locale"將返回同樣的資源。
或者,你能在路由模版中定義一個默認值,如下:
public class BooksController : ApiController { [Route("api/books/locale/{lcid=1033}")] public IEnumerable<Book> GetBooksByLocale(int lcid) { ... } }
這個示例幾乎和上面的例子一樣。但是當默認值被應用的時候,行為上有一個輕微的不同。
1、在第一個例子中("{lcid?}"),1033默認值被直接指定在方法參數上。因此這個參數將有一個精確的值。
2、在第二個例子中("{lcid?}"),1033默認值通過模型綁定進程。這個默認的模型綁定將轉換“1033”為1033數值。但是,你可以插入自定義模型綁定,其中可能做不同的事情。
在大多數情況下,除非在你的管道中有自定義的模型綁定,這兩種表現是等價的。
7、路由名稱
在Web API中,每個路由都有一個名稱。路由名稱被用於生成鏈接,你能在HTTP響應中包含一個鏈接。
指定這個路由名稱,在這個屬性上設置RouteName屬性。下面的例子展示怎樣設置路由名稱,當生成一個鏈接也能用這個路由名稱。
public class BooksController : ApiController { [Route("api/books/{id}", RouteName="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; } }
如果你不設置RouteName 屬性,Web API產生這個名字。這個默認的路由名稱是"ControllerName.ActionName"。在前面的例子中,對於這個GetBook
方法這個默認的路由名稱將是“Books.GetBook”。對於同一個動作名稱如果控制器有多重的屬性路由,一個后綴將被添加。例如,“Books.GetBook1" 和 "Books.GetBook2"。
8、路由順序
當一個框架試圖講一個URI匹配到路由的時候,它會在特定的順序下評估這些路由。為了指定這個順序,在路由屬性上設置RouteOrder 屬性。較低的值將首先被評估。這默認的順序值是0。
這里是如何確定的總排序:
1.比較路由屬性中的RouteName 屬性。
2.在路由模版中查看每個URI片段。對於每個片段,順序如下:
- 文本片段。
- 帶有約束的路由參數。
- 不帶有約束的路由參數。
- 帶有約束的通配符路由參數。
- 不帶有約束的通配符路由參數。
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) { ... } }
這些路由進行排序,如下所示:
1.orders/details 2.orders/{id} 3.orders/{customerName} 4.orders/{*date} 5.orders/pending
注意"details"是一個文本片段出現在"{id}"之前,但是"pending"將出現在最后因為這個RouteOrder 屬性是1。(這個例子假定沒有客戶命名為"details"或者"pending")。總之,試圖去避免模糊不清的路由。在這個例子中,GetByCustomer
的一個較好的路由模版是"customers/{customerName}")。
總結
屬性路由,很不錯,隨心所欲,想怎么定義很方便,真是一大亮點吧。
本文參考鏈接http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
本文同時也已經更新至系列導航中http://www.cnblogs.com/aehyok/p/3446289.html