ASP.NET Web API 2中的屬性路由

前言

路由是指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ò)程:

  1. 比較每個(gè)路由屬性的RouteOrder屬性
  2. 在路由模板上查找每個(gè)URI字段。對(duì)于每個(gè)字段,順序由以下因素確定:
  • 字面字段
  • 包含約束的路由參數(shù)
  • 不包含約束的路由參數(shù)
  • 包含約束的通配符參數(shù)字段
  • 不包含約束的通配符參數(shù)字段
  1. 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}”。)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,637評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 178,555評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,900評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,629評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,976評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評(píng)論 3 448
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 43,139評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,686評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,411評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,641評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,820評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 35,233評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 36,567評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,362評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,604評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容