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

路由 是 Web API 将 URI 与操作匹配的方式。 Web API 2 支持一种称为 属性路由的新型路由。 顾名思义,属性路由使用属性来定义路由。 通过属性路由,可以更好地控制 Web API 中的 URI。 例如,可以轻松创建描述资源层次结构的 URI。

旧式路由(称为基于约定的路由)仍完全受支持。 事实上,可以将这两种技术合并到同一项目中。

本主题演示如何启用属性路由,并介绍属性路由的各种选项。 有关使用属性路由的端到端教程,请参阅 在 Web API 2 中使用属性路由创建 REST API

先决条件

Visual Studio 2017 社区版、专业版或企业版

或者,使用 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”是订单号,但“挂起”映射到集合。

/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}
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 属性。 首先计算较低的值。 默认顺序值为零。

下面是确定总排序的方式:

  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。 (此示例假定没有名为“details”或“pending”的客户。通常,请尽量避免路由不明确。在此示例中,更好的 GetByCustomer 路由模板是“customers/{customerName}”)