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

邁克·瓦森by Mike Wasson

路由是 Web API 將 URI 與操作匹配的方式。Routing is how Web API matches a URI to an action. Web API 2 支援一種稱為屬性路由的新型路由。Web API 2 supports a new type of routing, called attribute routing. 顧名思義,屬性路由使用屬性來定義路由。As the name implies, attribute routing uses attributes to define routes. 屬性路由使您能夠對 Web API 中的 URI 進行更多控制。Attribute routing gives you more control over the URIs in your web API. 例如,您可以輕鬆地創建描述資源層次結構的 URI。For example, you can easily create URIs that describe hierarchies of resources.

早期路由樣式(稱為基於約定的路由)仍完全支援。The earlier style of routing, called convention-based routing, is still fully supported. 事實上,您可以將這兩種技術合併到同一專案中。In fact, you can combine both techniques in the same project.

本主題演示如何啟用屬性路由,並介紹屬性路由的各種選項。This topic shows how to enable attribute routing and describes the various options for attribute routing. 有關使用屬性路由的端到端教程,請參閱在 Web API 2 中使用屬性路由創建 REST API。For an end-to-end tutorial that uses attribute routing, see Create a REST API with Attribute Routing in Web API 2.

PrerequisitesPrerequisites

視覺工作室 2017社區版、專業版或企業版Visual Studio 2017 Community, Professional, or Enterprise edition

或者,使用 NuGet 包管理器安裝必要的包。Alternatively, use NuGet Package Manager to install the necessary packages. 從 Visual Studio 中的 「工具」功能表中,選擇NuGet 套件管理器,然後選擇 「包管理器控制台」。From the Tools menu in Visual Studio, select NuGet Package Manager, then select Package Manager Console. 在套件管理員主控台視窗中輸入以下指令:Enter the following command in the Package Manager Console window:

Install-Package Microsoft.AspNet.WebApi.WebHost

為什麼選擇屬性路由?Why Attribute Routing?

Web API 的第一個版本使用了基於約定的路由。The first release of Web API used convention-based routing. 在這種類型的路由中,定義一個或多個路由範本,這些範本基本上是參數化字串。In that type of routing, you define one or more route templates, which are basically parameterized strings. 當框架收到請求時,它將URI與路由範本匹配。When the framework receives a request, it matches the URI against the route template. 有關基於約定的路由的詳細資訊,請參閱ASP.NET Web API 中的路由For more information about convention-based routing, see Routing in ASP.NET Web API.

基於約定的路由的一個優點是範本在一個位置定義,並且路由規則在所有控制器之間一致應用。One advantage of convention-based routing is that templates are defined in a single place, and the routing rules are applied consistently across all controllers. 遺憾的是,基於約定的路由很難支援 RESTful API 中常見的某些 URI 模式。Unfortunately, convention-based routing makes it hard to support certain URI patterns that are common in RESTful APIs. 例如,資源通常包含子資源:客戶有訂單,電影有演員,書籍有作者,等等。For example, resources often contain child resources: Customers have orders, movies have actors, books have authors, and so forth. 建立反映這些關係的 URI 是很自然的:It's natural to create URIs that reflect these relations:

/customers/1/orders

這種類型的URI很難使用基於約定的路由創建。This type of URI is difficult to create using convention-based routing. 儘管可以做到這一點,但如果您有許多控制器或資源類型,則結果不會很好地擴展。Although it can be done, the results don't scale well if you have many controllers or resource types.

使用屬性路由,為此URI定義路由非常簡單。With attribute routing, it's trivial to define a route for this URI. 只需向控制器操作新增屬性:You simply add an attribute to the controller action:

[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }

下面是一些其他模式,屬性路由使容易。Here are some other patterns that attribute routing makes easy.

API 版本控制API versioning

在此示例中,"/api/v1/產品"將路由到與"/api/v2/產品"不同的控制器。In this example, "/api/v1/products" would be routed to a different controller than "/api/v2/products".

/api/v1/products /api/v2/products

重載 URI 段Overloaded URI segments

在此示例中,「1」是訂單號,但「掛起」映射到集合。In this example, "1" is an order number, but "pending" maps to a collection.

/orders/1 /orders/pending

多種參數類型Multiple parameter types

在此示例中,"1"是訂單號,但"2013/06/16"指定日期。In this example, "1" is an order number, but "2013/06/16" specifies a date.

/orders/1 /orders/2013/06/16

開啟屬性路由Enabling Attribute Routing

要啟用屬性路由,請在配置期間調用MapHttpAttributeRoutes。To enable attribute routing, call MapHttpAttributeRoutes during configuration. 此擴充方法在System.Web.http.HTTP:設定擴充套件中定義。This extension method is defined in the System.Web.Http.HttpConfigurationExtensions class.

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

屬性路由可以與基於約定的路由組合使用。Attribute routing can be combined with convention-based routing. 要定義基於約定的路由,請調用MapHTTPRoute方法。To define convention-based routes, call the MapHttpRoute method.

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 2For more information about configuring Web API, see Configuring ASP.NET Web API 2.

注意:從 Web API 1 移轉Note: Migrating From Web API 1

在 Web API 2 之前,Web API 專案樣本產生了如下所示的代碼:Prior to Web API 2, the Web API project templates generated code like this:

protected void Application_Start()
{
    // WARNING - Not compatible with attribute routing.
    WebApiConfig.Register(GlobalConfiguration.Configuration);
}

如果啟用了屬性路由,則此代碼將引發異常。If attribute routing is enabled, this code will throw an exception. 如果將現有 Web API 專案升級為使用屬性路由,請確保將此設定代碼更新為以下內容:If you upgrade an existing Web API project to use attribute routing, make sure to update this configuration code to the following:

protected void Application_Start()
{
    // Pass a delegate to the Configure method.
    GlobalConfiguration.Configure(WebApiConfig.Register);
}

Note

有關詳細資訊,請參閱使用託管ASP.NET配置 Web API。For more information, see Configuring Web API with ASP.NET Hosting.

新增路由屬性Adding Route Attributes

下面是使用屬性定義的路由的範例:Here is an example of a route defined using an attribute:

public class OrdersController : ApiController
{
    [Route("customers/{customerId}/orders")]
    [HttpGet]
    public IEnumerable<Order> FindOrdersByCustomer(int customerId) { ... }
}

字串"客戶/[客戶 Id]/"訂單 是路由的 URI 範本。The string "customers/{customerId}/orders" is the URI template for the route. Web API 嘗試將請求 URI 與範本匹配。Web API tries to match the request URI to the template. 在此示例中,"客戶"和"訂單"是文本段,"[客戶Id]"是一個變數參數。In this example, "customers" and "orders" are literal segments, and "{customerId}" is a variable parameter. 以下 URI 將符合此樣本:The following URIs would match this template:

  • http://localhost/customers/1/orders
  • http://localhost/customers/bob/orders
  • http://localhost/customers/1234-5678/orders

您可以使用約束來限制匹配,本主題稍後將介紹。You can restrict the matching by using constraints, described later in this topic.

請注意,"製程路線樣本中的"{customerId} 參數與 方法中的customerId參數的名稱匹配。Notice that the "{customerId}" parameter in the route template matches the name of the customerId parameter in the method. 當 Web API 呼叫控制器操作時,它嘗試綁定路由參數。When Web API invokes the controller action, it tries to bind the route parameters. 例如,如果 URIhttp://example.com/customers/1/orders是 ,Web API 會嘗試在操作中將值「1」綁定到customerId參數。For example, if the URI is http://example.com/customers/1/orders, Web API tries to bind the value "1" to the customerId parameter in the action.

URI 樣本可以具有多個參數:A URI template can have several parameters:

[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }

沒有任何路由屬性的控制器方法都使用基於約定的路由。Any controller methods that do not have a route attribute use convention-based routing. 這樣,可以將兩種類型的路由合併到同一專案中。That way, you can combine both types of routing in the same project.

HTTP 方法HTTP Methods

Web API 還根據請求的 HTTP 方法(GET、POST 等)選擇操作。Web API also selects actions based on the HTTP method of the request (GET, POST, etc). 默認情況下,Web API 查找與控制器方法名稱開頭不區分大小寫的匹配。By default, Web API looks for a case-insensitive match with the start of the controller method name. 例如,名為的PutCustomers控制器方法與 HTTP PUT 請求匹配。For example, a controller method named PutCustomers matches an HTTP PUT request.

您可以通過使用以下任何屬性修飾方法來重寫此約定:You can override this convention by decorating the method with any the following attributes:

  • [ HttpDelete][HttpDelete]
  • [HttpGet][HttpGet]
  • [HTTPHead][HttpHead]
  • 【 HttpOptions][HttpOptions]
  • [HTTPPatch][HttpPatch]
  • [ HTTPPost][HttpPost]
  • [HttpPut][HttpPut]

下面的範例將 CreateBook 方法映射到 HTTP POST 請求。The following example maps the CreateBook method to HTTP POST requests.

[Route("api/books")]
[HttpPost]
public HttpResponseMessage CreateBook(Book book) { ... }

對於所有其他 HTTP 方法(包括非標準方法),請使用AcceptVerbs屬性,該屬性採用 HTTP 方法的清單。For all other HTTP methods, including non-standard methods, use the AcceptVerbs attribute, which takes a list of HTTP methods.

// WebDAV method
[Route("api/books")]
[AcceptVerbs("MKCOL")]
public void MakeCollection() { }

路由前置碼Route Prefixes

通常,控制器中的路由都以相同的首碼開頭。Often, the routes in a controller all start with the same prefix. 例如:For example:

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] 屬性為整個控制器設定公共前置前置字串:You can set a common prefix for an entire controller by using the [RoutePrefix] attribute:

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

在方法屬性上使用波浪線 (*) 來覆蓋路由首碼:Use a tilde (~) on the method attribute to override the route prefix:

[RoutePrefix("api/books")]
public class BooksController : ApiController
{
    // GET /api/authors/1/books
    [Route("~/api/authors/{authorId:int}/books")]
    public IEnumerable<Book> GetByAuthor(int authorId) { ... }

    // ...
}

路由前置字串可以包含參數:The route prefix can include parameters:

[RoutePrefix("customers/{customerId}")]
public class OrdersController : ApiController
{
    // GET customers/1/orders
    [Route("orders")]
    public IEnumerable<Order> Get(int customerId) { ... }
}

路由約束Route Constraints

通過路由約束,您可以限制路由範本中的參數的匹配方式。Route constraints let you restrict how the parameters in the route template are matched. 一般語法是"[參數:"約束 ] 。The general syntax is "{parameter:constraint}". 例如:For example:

[Route("users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("users/{name}")]
public User GetUserByName(string name) { ... }

此處,僅當URI的ID""段是整數時,才會選擇第一個路由。Here, the first route will only be selected if the "id" segment of the URI is an integer. 否則,將選擇第二個路由。Otherwise, the second route will be chosen.

下表列出了支援的約束。The following table lists the constraints that are supported.

條件約束Constraint 描述Description 範例Example
alphaalpha 符合大寫或小寫拉丁字母字元(a-z,A-Z)Matches uppercase or lowercase Latin alphabet characters (a-z, A-Z) [x:阿爾法]{x:alpha}
boolbool 匹配布爾值。Matches a Boolean value. {x:布爾]{x:bool}
Datetimedatetime 匹配日期時間值。Matches a DateTime value. [x:日期時間]{x:datetime}
decimaldecimal 匹配十進位值。Matches a decimal value. [x:十進位]{x:decimal}
doubledouble 匹配64位浮點值。Matches a 64-bit floating-point value. [x:雙]{x:double}
FLOATfloat 匹配 32 位浮點值。Matches a 32-bit floating-point value. [x:浮動]{x:float}
guidguid 匹配 GUID 值。Matches a GUID value. {x:吉德]{x:guid}
intint 匹配 32 位整數值。Matches a 32-bit integer value. {x:int}{x:int}
長度length 將字串與指定長度或指定長度範圍內的字串匹配。Matches a string with the specified length or within a specified range of lengths. [x:長度(6)][x:長度(1,20)]{x:length(6)} {x:length(1,20)}
longlong 匹配64位整數值。Matches a 64-bit integer value. [x:長]{x:long}
maxmax 匹配具有最大值的整數。Matches an integer with a maximum value. {x:最大(10)]{x:max(10)}
最大長度maxlength 匹配具有最大長度的字串。Matches a string with a maximum length. {x:最大長度(10)]{x:maxlength(10)}
Minmin 匹配具有最小值的整數。Matches an integer with a minimum value. {x:min(10)]{x:min(10)}
最小長度minlength 匹配長度最小的字串。Matches a string with a minimum length. {x:最小長度(10)]{x:minlength(10)}
rangerange 匹配值範圍內的整數。Matches an integer within a range of values. [x:範圍(10,50)]{x:range(10,50)}
RegExregex 匹配正則表達式。Matches a regular expression. {x:regex(\d{3}-d{3}-\d{4}$)]{x:regex(^\d{3}-\d{3}-\d{4}$)}

請注意,某些約束(如"最小"值)在括弧中採用參數。Notice that some of the constraints, such as "min", take arguments in parentheses. 您可以將多個約束應用於參數,由冒號分隔。You can apply multiple constraints to a parameter, separated by a colon.

[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { ... }

自訂路由限制式Custom Route Constraints

您可以通過實現IHTTPRoute 約束介面來創建自訂路由約束。You can create custom route constraints by implementing the IHttpRouteConstraint interface. 例如,以下約束將參數限制為非零整數值。For example, the following constraint restricts a parameter to a non-zero integer value.

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;
    }
}

以下代碼展示如何註冊約束:The following code shows how to register the constraint:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var constraintResolver = new DefaultInlineConstraintResolver();
        constraintResolver.ConstraintMap.Add("nonzero", typeof(NonZeroConstraint));

        config.MapHttpAttributeRoutes(constraintResolver);
    }
}

現在,您可以在路由中應用約束:Now you can apply the constraint in your routes:

[Route("{id:nonzero}")]
public HttpResponseMessage GetNonZero(int id) { ... }

您還可以通過實現IInline 約束解析器介面來替換整個預設內聯約束解析器類。You can also replace the entire DefaultInlineConstraintResolver class by implementing the IInlineConstraintResolver interface. 這樣做將替換所有內置約束,除非您的IInline 約束解析器的實現專門添加它們。Doing so will replace all of the built-in constraints, unless your implementation of IInlineConstraintResolver specifically adds them.

選擇的 URI 參數與預設值Optional URI Parameters and Default Values

您可以通過向路由參數添加問號來使 URI 參數成為可選參數。You can make a URI parameter optional by adding a question mark to the route parameter. 如果路由參數是可選的,則必須為方法參數定義預設值。If a route parameter is optional, you must define a default value for the method parameter.

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int?}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid = 1033) { ... }
}

在此範例中,/api/books/locale/1033``/api/books/locale並傳回相同的資源。In this example, /api/books/locale/1033 and /api/books/locale return the same resource.

或者,您可以在工藝路線範本中指定預設值,如下所示:Alternatively, you can specify a default value inside the route template, as follows:

public class BooksController : ApiController
{
    [Route("api/books/locale/{lcid:int=1033}")]
    public IEnumerable<Book> GetBooksByLocale(int lcid) { ... }
}

這幾乎與前一個示例相同,但在應用預設值時,行為有細微差異。This is almost the same as the previous example, but there is a slight difference of behavior when the default value is applied.

  • 在第一個示例("{lcid:int?}")中,默認值 1033 直接分配給方法參數,因此參數將具有此精確值。In the first example ("{lcid:int?}"), the default value of 1033 is assigned directly to the method parameter, so the parameter will have this exact value.
  • 在第二個示例("{lcid:int=1033})中,"1033"的默認值通過模型綁定過程。In the second example ("{lcid:int=1033}"), the default value of "1033" goes through the model-binding process. 預設模型綁定器將「1033」轉換為數值 1033。The default model-binder will convert "1033" to the numeric value 1033. 但是,您可以插入自定義模型活頁夾,這可能執行不同操作。However, you could plug in a custom model binder, which might do something different.

(在大多數情況下,除非您管道中有自定義模型綁定器,否則這兩個窗體將等效。(In most cases, unless you have custom model binders in your pipeline, the two forms will be equivalent.)

路由名稱Route Names

在 Web API 中,每個路由都有一個名稱。In Web API, every route has a name. 路由名稱可用於生成連結,因此您可以在 HTTP 回應中包含連結。Route names are useful for generating links, so that you can include a link in an HTTP response.

要指定路由名稱,請在屬性上設定Name屬性。To specify the route name, set the Name property on the attribute. 下面的範例示範如何設置路由名稱,以及生成連結時如何使用路由名稱。The following example shows how to set the route name, and also how to use the route name when generating a link.

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

當框架嘗試將URI與路由匹配時,它會按特定順序評估路由。When the framework tries to match a URI with a route, it evaluates the routes in a particular order. 要指定訂單,請在工藝路線屬性上設置Order屬性。To specify the order, set the Order property on the route attribute. 首先計算較低的值。Lower values are evaluated first. 默認訂單值為零。The default order value is zero.

以下是確定總排序的方式:Here is how the total ordering is determined:

  1. 比較工藝路線屬性的Order屬性。Compare the Order property of the route attribute.

  2. 查看工藝路線範本中的每個 URI 段。Look at each URI segment in the route template. 對於每個段,順序如下:For each segment, order as follows:

    1. 欄位。Literal segments.
    2. 具有約束的路由參數。Route parameters with constraints.
    3. 路由參數沒有約束。Route parameters without constraints.
    4. 具有約束的通配符參數段。Wildcard parameter segments with constraints.
    5. 通配符參數段沒有約束。Wildcard parameter segments without constraints.
  3. 在連接的情況下,路由由路由範本的區分大小寫序字串比較(OrdinalIgnoreCase)排序。In the case of a tie, routes are ordered by a case-insensitive ordinal string comparison (OrdinalIgnoreCase) of the route template.

範例如下。Here is an example. 假設您定義以下控制器:Suppose you define the following controller:

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

這些路線按如下方式排序。These routes are ordered as follows.

  1. 訂單/詳細資訊orders/details
  2. 訂單/{id}orders/{id}
  3. 訂單/[客戶名稱]orders/{customerName}
  4. 訂單/*日期orders/{*date}
  5. 訂單/待定orders/pending

請注意,"詳細資訊"是文本段,出現在"{id}"之前,但"掛起"顯示最後,因為Order屬性為 1。Notice that "details" is a literal segment and appears before "{id}", but "pending" appears last because the Order property is 1. (此示例假定沒有名為"詳細資訊"或"掛起"的客戶。(This example assumes there are no customers named "details" or "pending". 通常,盡量避免不明確的路線。In general, try to avoid ambiguous routes. 在此示例中,更好的GetByCustomer路由範本是"客戶/[客戶名稱]"In this example, a better route template for GetByCustomer is "customers/{customerName}" )