路由(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匹配路由模版時是從路由的排列順序開始匹配,一旦匹配成功則會忽略後面的路由模版了。