ASP.NET Web API 2 中的屬性路由
路由是 Web API 如何比對 URI 與動作。 Web API 2 支援新的路由類型,稱為屬性路由。 正如名稱所暗示,屬性路由會使用屬性來定義路由。 屬性路由讓您對 Web API 中的 URI 擁有更大的控制權。 例如,您可以輕鬆地建立描述資源階層的 URI。
仍完全支援先前的路由樣式,稱為慣例型路由。 事實上,您可以在相同的專案中合併這兩種技術。
本主題說明如何啟用屬性路由,並描述屬性路由的各種選項。 如需使用屬性路由的端對端教學課程,請參閱在 Web API 2 中使用屬性路由建立 REST API。
必要條件
Visual Studio 2017 Community、Professional 或 Enterprise 版。
或者,使用 NuGet 套件管理員來安裝必要的套件。 從 Visual Studio 的「工具」功能表中,選擇「NuGet 套件管理員」,然後選擇「套件管理員主控台」。 在「套件管理員主控台」視窗中,輸入以下命令:
Install-Package Microsoft.AspNet.WebApi.WebHost
為什麼是屬性路由?
Web API 的第一個版本使用慣例型路由。 在該類型的路由中,您可以定義一或多個路由範本,這些範本基本上是參數化的字串。 當架構收到要求時,它會比對路由範本的 URI。 如需慣例型路由的詳細資訊,請參閱 ASP.NET Web API 中的路由。
慣例型路由的其中一個優點是範本是在單一位置定義,而且路由規則會一致套用到所有控制器。 不幸的是,以慣例型路由很難支援 RESTful API 中常見的特定 URI 模式。 例如,資源通常包含子資源:客戶有訂單、電影有演員、書籍有作者等等。 建立反映這些關係的 URI 是很自然的:
/customers/1/orders
這種類型的 URI 很難使用慣例型路由來建立。 雖然可以完成,但如果您有許多控制器或資源類型,結果不會調整良好。
使用屬性路由時,定義此 URI 的路由是很簡單的。 您只需將屬性新增至控制器動作:
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
以下是屬性路由讓一些其他模式變得容易。
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
啟用屬性路由
若要啟用屬性路由,請在設定期間呼叫 MapHttpAttributeRoutes。 這個擴充方法定義於 System.Web.Http.HttpConfigurationExtensions 類別中。
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.
}
}
}
屬性路由可以與慣例型路由結合。 若要定義慣例型路由,請呼叫 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 }
);
}
}
如需設定 Web API 的詳細資訊,請參閱設定 ASP.NET Web API 2。
注意:從 Web API 1 移轉
在 Web API 2 之前,Web API 專案範本會產生如下的程式碼:
protected void Application_Start()
{
// WARNING - Not compatible with attribute routing.
WebApiConfig.Register(GlobalConfiguration.Configuration);
}
如果已啟用屬性路由,此程式碼將會擲回例外狀況。 如果您將現有的 Web API 專案升級為使用屬性路由,請務必將此設定程式碼更新為下列:
protected void Application_Start()
{
// Pass a delegate to the Configure method.
GlobalConfiguration.Configure(WebApiConfig.Register);
}
注意
如需詳細資訊,請參閱使用 ASP.NET 裝載設定 Web API。
新增路由屬性
以下是使用屬性定義的路由範例:
public class OrdersController : ApiController
{
[Route("customers/{customerId}/orders")]
[HttpGet]
public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}
"customers/{customerId}/orders" 字串是路由的 URI 範本。 Web API 會嘗試比對要求 URI 與範本。 在此範例中,"customers" 和 "orders" 是常值區段,而 "{customerId}" 是變數參數。 下列 URI 會符合此範本:
http://localhost/customers/1/orders
http://localhost/customers/bob/orders
http://localhost/customers/1234-5678/orders
您可以使用本主題稍後所述的條件約束來限制比對。
請注意,路由範本中的 "{customerId}" 參數符合方法中 customerId 參數的名稱。 當 Web API 叫用控制器動作時,它會嘗試繫結路由參數。 例如,如果 URI 是 http://example.com/customers/1/orders
,Web API 會嘗試將值「1」繫結至動作中的 customerId 參數。
URI 範本可以有數個參數:
[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }
沒有任何路由屬性的控制器方法都使用慣例型路由。 如此一來,您就可以在相同的專案中合併這兩種類型的路由。
HTTP 方法
Web API 也會根據要求的 HTTP 方法來選取動作 (GET、POST 等)。 根據預設,Web API 會尋找與控制器方法名稱開頭不區分大小寫的相符項目。 例如,名為 PutCustomers
的控制器方法符合 HTTP PUT 要求。
您可以使用下列任何屬性來裝飾方法,以覆寫此慣例:
- [HttpDelete]
- [HttpGet]
- [HttpHead]
- [HttpOptions]
- [HttpPatch]
- [HttpPost]
- [HttpPut]
在下列範例中,Web API 會將 CreateBook 方法對應至 HTTP POST 要求。
[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }
對於所有其他 HTTP 方法,包括非標準方法,請使用 AcceptVerbs 屬性,它會採用 HTTP 方法的清單。
// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }
路由前置詞
控制器中的路由通常會以相同的前置詞開頭。 例如:
public class BooksController : ApiController
{
[Route("api/books")]
public IEnumerable<Book> GetBooks() { ... }
[Route("api/books/{id:int}")]
public Book GetBook(int id) { ... }
[Route("api/books")]
[HttpPost]
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) { ... }
}
在方法屬性上使用 tilde (~) 來覆寫路由前置詞:
[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) { ... }
在此,只有在 URI 的「id」區段是整數時,才會選取第一個路由。 否則,將會選取第二個路由。
下表列出了支援的條件約束。
條件約束 | 描述 | 範例 |
---|---|---|
alpha | 比對大寫或小寫拉丁字母字元 (a-z,A-Z) | {x:alpha} |
bool | 比對布爾值。 | {x:bool} |
Datetime | 比對 DateTime 值。 | {x:datetime} |
decimal | 比對十進位值。 | {x:decimal} |
double | 比對 64 位元浮點值。 | {x:double} |
float | 比對 32 位元浮點值。 | {x:float} |
guid | 比對 GUID 值。 | {x:guid} |
int | 比對 32 位元整數值。 | {x:int} |
length | 比對具有指定長度的字串,或在指定的長度範圍內。 | {x:length(6)} {x:length(1,20)} |
long | 比對 64 位元整數值。 | {x:long} |
max | 比對具有最大值的整數。 | {x:max(10)} |
maxlength | 比對長度上限的字串。 | {x:maxlength(10)} |
分鐘 | 比對具有最小值的整數。 | {x:min(10)} |
minlength | 比對長度下限的字串。 | {x:minlength(10)} |
range | 比對值範圍內的整數。 | {x:range(10,50)} |
RegEx | 比對規則運算式。 | {x:regex(^\d{3}-\d{3}-\d{4}$)} |
請注意,某些條件約束,例如「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) { ... }
您也可以藉由實作 IInlineConstraintResolver介面來取代整個 DefaultInlineConstraintResolver 類別。 這麼做將會取代所有內建條件約束,除非您實作 IInlineConstraintResolver 特別新增它們。
選擇性 URI 參數和預設值
您可以將問號新增至路由參數,讓 URI 參數成為選擇性參數。 如果路由參數是選擇性的,您必須定義方法參數的預設值。
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?}") 中,預設值 1033 直接指派給方法參數,因此該參數將具有該精確值。
- 在第二個範例中 ("{lcid:int=1033}"),預設值「1033」會經過模型繫結程序。 預設模型繫結器會將「1033」轉換為數值 1033。 不過,您可以插入自訂模型繫結器,這可能會執行不同的動作。
(在大部分情況下,除非您在管線中有自訂模型繫結器,否則這兩個形式會相等。)
路由名稱
在 Web API 中,每個路由都有名稱。 路由名稱可用於產生連結,因此您可以在 HTTP 回應中包含連結。
若要指定路由名稱,請在屬性上設定 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 與路由時,它會以特定順序評估路由。 若要指定順序,請在路由屬性上設定 Order 屬性。 先評估較低的值。 預設順序值為零。
以下是決定總排序的方式:
比較路由屬性的 Order 屬性。
查看路由範本中的每個 URI 區段。 針對每個區段,順序如下:
- 常值區段。
- 使用條件約束路由參數。
- 不含條件約束的路由參數。
- 含條件約束的萬用字元參數區段。
- 不含條件約束的萬用字元參數區段。
在繫結的情況下,路由會依路由範本的不區分大小寫序數字串比較 (OrdinalIgnoreCase) 排序。
以下是範例。 假設您定義下列控制器:
[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" 是常值區段,出現在 "{id}" 之前,但 "pending" 最後出現,因為 Order 屬性是 1。 (此範例假設沒有名為 "details" 或 "pending" 的客戶。一般而言,請嘗試避免模棱兩可的路由。在此範例中,GetByCustomer
的更好路由範本是 "customers/{customerName}")