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) { ... }
}

在 方法屬性上使用波浪線 (~) 覆寫路由前置詞:

[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}
長度 比對具有指定長度或指定長度範圍內的字串。 {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 屬性。 系統會先評估較低的值。 預設順序值為零。

以下是決定總排序的方式:

  1. 比較路由屬性的 Order 屬性。

  2. 查看路由範本中的每個 URI 區段。 針對每個區段,順序如下:

    1. 常值區段。
    2. 具有條件約束的路由參數。
    3. 沒有條件約束的路由參數。
    4. 具有條件約束的萬用字元參數區段。
    5. 不含條件約束的萬用字元參數區段。
  3. 在系結的情況下,路由會依不區分大小寫的序數位符串比較來排序, (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) { ... }
}

這些路由的排序方式如下。

  1. orders/details
  2. orders/{id}
  3. orders/{customerName}
  4. orders/{*date}
  5. orders/pending

請注意,「details」 是常值區段,出現在 「{id}」 之前,但 「pending」 最後會出現,因為 Order 屬性為 1。 (本範例假設沒有名為「詳細資料」或「擱置」的客戶。一般而言,請嘗試避免模棱兩可的路由。在此範例中,較佳的 GetByCustomer 路由範本是 「customers/{customerName}」 )