前言
路由是指Web API如何匹配到具體的動(dòng)作。Web API 2支持一個(gè)新的路由類(lèi)型,它被稱(chēng)為屬性路由。正如其名,屬性路由使用屬性來(lái)定義路由。屬性路由給予你在web API的URI上的更多控制。例如,你能輕易的創(chuàng)建用于描述層級(jí)資源的URI。
早期的路由風(fēng)格被稱(chēng)為基于約定的路由,現(xiàn)在仍然被完整支持,你可以將這兩種技術(shù)用于同一個(gè)項(xiàng)目中。
本主題演示如何啟用屬性的路由,并描述屬性路由的各種選項(xiàng)。關(guān)于使用屬性路由的實(shí)戰(zhàn)教程,請(qǐng)查看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來(lái)安裝必要的包。在Visual Studio的Tools目錄下,選擇Library
Package Manager,然后選擇Package Manager Console。在Package Manager
Console窗口輸入以下命令:
Install-Package Microsoft.AspNet.WebApi.WebHost
Why Attribute Routing?
Web
API的首個(gè)發(fā)行版使用基于約定的路由。在那種路由中,你定義一個(gè)或多個(gè)路由模板,它們是一些基本的參數(shù)字符串。當(dāng)框架收到一個(gè)請(qǐng)求時(shí),它會(huì)將URI匹配
到路由模板中。(關(guān)于基于約定的路由的更多信息,請(qǐng)查看Routing in ASP.NET Web API)
基于約定的路由的一個(gè)優(yōu)勢(shì)是模板是定義在單一地方的,并且路由規(guī)則會(huì)被應(yīng)用到所有的控制器。不幸的是,基于約定的路由很難去支持一個(gè)在
RESTful
API中很常見(jiàn)的URI模式。例如,資源通常包含著子資源:客戶包含著訂單,電影包含著演員,書(shū)籍包含著作者等等。所以很自然地創(chuàng)建映射這些關(guān)系的
URI:
/customers/1/orders
有了屬性路由,就可以很輕易地定義一個(gè)針對(duì)該URI的路由。你只需要簡(jiǎn)單的添加一個(gè)屬性到控制器動(dòng)作上:
[Route("customers/{customerId}/orders")]
public IEnumerableGetOrdersByCustomer(int customerId) { ... }
這里還有些因?yàn)橛辛藢傩月酚啥兊酶尤菀椎钠渌J剑?/p>
API versioning
在本例中,”api/v1/products”相對(duì)于”api/v2/products”可能會(huì)路由到不同的控制器。
/api/v1/products
/api/v2/products
Overloaded URI segments
在本例中,”1”是個(gè)訂單數(shù)字,但是“pending”映射到一個(gè)集合。
/orders/1
/orders/pending
Multiple parameter types
在本例中,“1”是個(gè)訂單數(shù)字,但是“2013/06/10”卻是個(gè)日期。
/orders/1
/orders/2013/06/10
啟用屬性路由(Enabling Attribute Routing)
為了啟用屬性路由,需要在配置時(shí)調(diào)用MapHttpAttributeRoutes。這個(gè)擴(kuò)展方法被定義在System.Web.Http.HttpConfigurationExtensions類(lèi)中。
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.
}
}
}
屬性路由也可以和基于約定的路由結(jié)合起來(lái)。為了定義基于約定的路由,調(diào)用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 }
);
}
}
關(guān)于配置Web API的更多信息,請(qǐng)查看Configuring ASP.NET Web API 2。
在Web API 2之前,Web API項(xiàng)目目標(biāo)生成的代碼像是這樣:
protected void Application_Start()
{
// WARNING - Not compatible with attribute routing.
WebApiConfig.Register(GlobalConfiguration.Configuration);
}
如果屬性路由沒(méi)有被啟用,這個(gè)代碼將會(huì)拋出異常。如果你升級(jí)一個(gè)已有的Web API項(xiàng)目來(lái)使用屬性路由,請(qǐng)確保像下面這樣升級(jí)了配置代碼:
protected void Application_Start()
{
// Pass a delegate to the Configure method.
GlobalConfiguration.Configure(WebApiConfig.Register);
}
備注:關(guān)于更多信息,請(qǐng)查看Configuring Web API with ASP.NET Hosting
添加路由屬性(Adding Route Attributes)
這里是一個(gè)使用屬性定義路由的示例:
public class OrdersController : ApiController
{
[Route("customers/{customerId}/orders")]
[HttpGet]
public IEnumerableFindOrdersByCustomer(int customerId) { ... }
}
字符串“customers/{customerId}/orders”是一個(gè)用于路由的URI模板。Web
API會(huì)盡力將請(qǐng)求的URI匹配到模板中。在本例中,”customers“和”orders“都是字面字段,而”{customerId}”是變量參
數(shù)。以下這些URI會(huì)匹配這個(gè)模板:
1, http://localhost/customers/1/orders
2, http://localhost/customers/bob/orders
3, http://localhost/customer/1234-5678/orders
你能夠使用約束來(lái)限制這些匹配,這將會(huì)在本主題的后面進(jìn)行介紹。
注意到路由模板“{customerId}”參數(shù)匹配到方法中的customerId參數(shù)名。當(dāng)Web
API執(zhí)行控制器動(dòng)作時(shí),它會(huì)盡力綁定路由參數(shù)。例如,當(dāng)URI是 http: //example.com/customers/1/orders
時(shí),Web API會(huì)盡力將值”1“和動(dòng)作中的customerId參數(shù)進(jìn)行綁定。
一個(gè)URI模板可以有多個(gè)參數(shù):
[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }
任何沒(méi)有路由屬性的控制器方法都使用基于約定的路由。在此基礎(chǔ)上,你能夠在同一個(gè)項(xiàng)目中同時(shí)使用這兩種路由類(lèi)型。
HTTP Methods
Web API也會(huì)基于HTTP方法的請(qǐng)求(GET、POST等)來(lái)選擇動(dòng)作。默認(rèn)地,Web API會(huì)根據(jù)控制器方法名且不區(qū)分大小寫(xiě)地查找匹配。例如,一個(gè)控制器方法名為PutCustomers,它匹配一個(gè)HTTP的PUT請(qǐng)求。
你也可以通過(guò)給方法加上這些屬性來(lái)重載這個(gè)規(guī)則:
? [HttpDelete]
? [HttpGet]
? [HttpHead]
? [HttpOptions]
? [HttpPatch]
? [HttpPost]
? [HttpPut]
下面的例子映射CreateBook方法到HTTP的POST請(qǐng)求。
[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }
對(duì)于所有的HTTP方法,包括非標(biāo)準(zhǔn)方法,可以使用AcceptVerbs屬性,它需要傳入一個(gè)HTTP方法的列表。
// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }
路由前綴(Route Prefixes)
通常,控制器中的路由都以同樣的前綴開(kāi)始。例如:
public class BooksController : ApiController
{
[Route("api/books")]
public IEnumerableGetBooks() { ... }
[Route("api/books/{id:int}")]
public Book GetBook(int id) { ... }
[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }
}
你可以通過(guò)使用[RoutePrefix]屬性來(lái)為整個(gè)控制器設(shè)置一個(gè)公共前綴。
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
// GET api/books
[Route("")]
public IEnumerableGet() { ... }
// GET api/books/5
[Route("{id:int}")]
public Book Get(int id) { ... }
// POST api/books
[Route("")]
public HttpResponseMessage Post(Book book) { ... }
}
使用在方法屬性上使用一個(gè)通配符(~)來(lái)重載路由前綴。
[RoutePrefix("api/books")]
public class BooksController : ApiController
{
// GET /api/authors/1/books
[Route("~/api/authors/{authorId:int}/books")]
public IEnumerableGetByAuthor(int authorId) { ... }
// ...
}
路由前綴也可以包含參數(shù):
[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
// GET customers/1/orders
[Route("orders")]
public IEnumerableGet(int customerId) { ... }
}
路由約束(Route Constraints)
路由約束能夠讓你限制路由模板中的參數(shù)如何被匹配。大體的語(yǔ)法是“{parameter:constraint}”。例如:
[Route("users/{id:int}"]
public User GetUserById(int id) { ... }
[Route("users/{name}"]
public User GetUserByName(string name) { ... }
在這里,第一個(gè)路由只有當(dāng)URI的“id”字段是整型時(shí)才會(huì)被選擇。否則將會(huì)選擇第二個(gè)路由。
下表列出了被支持的約束。
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}$)} |
注意到其中一些約束在括號(hào)內(nèi)還需要參數(shù),比如“min”。你可以應(yīng)用多個(gè)約束到一個(gè)參數(shù),通過(guò)冒號(hào)分隔。
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }
自定義路由約束(Custom Route Constraints)
你可以通過(guò)實(shí)現(xiàn)IHttpRouteConstraint接口來(lái)創(chuàng)建一個(gè)自定義路由約束。例如,以下約束限制了一個(gè)參數(shù)到非零整型值。
public class NonZeroConstraint : IHttpRouteConstraint
{
public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
IDictionaryvalues, 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;
}
}
下面的代碼展示了如何去注冊(cè)約束:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));
config.MapHttpAttributeRoutes(constraintResolver);
}
}
現(xiàn)在你可以將該約束應(yīng)用到你的路由中了:
[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }
你也可以通過(guò)實(shí)現(xiàn)IInlineConstraintResolver接口來(lái)替換整個(gè)DefaultInlineConstraintResolver類(lèi)。這樣做會(huì)替換掉所有的內(nèi)建約束,除非你實(shí)現(xiàn)的IInlineConstraintResolver特意添加了它們。
可選的URI參數(shù)和默認(rèn)值
你可以通過(guò)添加問(wèn)好標(biāo)記到路由參數(shù)讓一個(gè)URI參數(shù)變成可選的。如果一個(gè)路由參數(shù)是可選的,你必須為方法參數(shù)定義默認(rèn)值。
public class BooksController : ApiController
{
[Route("api/books/locale/{lcid:int?}")]
public IEnumerableGetBooksByLocale(int lcid = 1033) { ... }
}
在本例中,/api/books/locale/1033和/api/books/locale會(huì)返回相同的資源。
或者,你可以特定一個(gè)默認(rèn)值在路由模板中,如下所示:
public class BooksController : ApiController
{
[Route("api/books/locale/{lcid:int=1033}")]
public IEnumerableGetBooksByLocale(int lcid) { ... }
}
這和前一個(gè)例子大體相同,但當(dāng)默認(rèn)值被應(yīng)用時(shí)存在細(xì)微差別。
1, 在第一個(gè)例子(“{Icid?}”),默認(rèn)值1033會(huì)被直接分配到方法參數(shù),所以參數(shù)將會(huì)擁有一個(gè)準(zhǔn)確的值。
2, 在第二個(gè)例子(“{Icid=1033}”),默認(rèn)值1033會(huì)通過(guò)模型綁定過(guò)程。默認(rèn)的模型綁定將會(huì)把1033轉(zhuǎn)換成數(shù)字值1033。然而,你可以遇到一個(gè)自定義的模型綁定,而這可能會(huì)出錯(cuò)。
(多數(shù)情況下,除非你在你的管道中有自定義模型綁定,否則這兩只表單形式是等價(jià)的。)
路由名稱(chēng)(Route Names)
在Web API中,每種路由都有一個(gè)名稱(chēng)。路由名稱(chēng)對(duì)于生成鏈接是非常有用的,正因此你才能在HTTP相應(yīng)中包含一個(gè)鏈接。
為了指定路由名稱(chēng),在屬性上(attribute)設(shè)置Name屬性(property)。以下示例展示了如何選擇一個(gè)路由名稱(chēng),以及當(dāng)生成一個(gè)鏈接時(shí)如何使用路由名稱(chēng)。
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)
當(dāng)框架試圖用路由匹配URI時(shí),它會(huì)得到一個(gè)特定的路由順序。為了指定順序,在路由屬性上設(shè)置RouteOrder屬性。小寫(xiě)的值在前,默認(rèn)順序值是零。
以下是如何確定所有的順序的過(guò)程:
- 比較每個(gè)路由屬性的RouteOrder屬性
- 在路由模板上查找每個(gè)URI字段。對(duì)于每個(gè)字段,順序由以下因素確定:
- 字面字段
- 包含約束的路由參數(shù)
- 不包含約束的路由參數(shù)
- 包含約束的通配符參數(shù)字段
- 不包含約束的通配符參數(shù)字段
- In the case of a tie,路由的順序由路由模板的不區(qū)分大小寫(xiě)的原始字符串比較來(lái)確定。
這是一個(gè)示例。假定你定義如下控制器:
[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”是一個(gè)字面字段,并且出現(xiàn)在“{id}”的前面,而“pending”出現(xiàn)在最后是因?yàn)樗腞outeOrder是1。
(這個(gè)例子假定不存在customer被命名為”details”和“pending”。通常來(lái)說(shuō),要盡量避免含糊不清的路由。在本例中,對(duì)于
GetByCustomer的一個(gè)更好的路由模板是”customers/{customerName}”。)