2.3属性在 ASP.NET Web API 2 路由

路由是 Web API 如何匹配 URI 的行动。Web API 2 支持一种新型的路由,称为属性路由顾名思义,属性路由使用属性来定义路由。属性路由给你更多的控制 Uri 在您的 web API。例如,您可以轻松创建描述层次结构的资源的 Uri。

早些时候的风格的路由,称为基于公约的路由,仍然完全支持。事实上,你可以结合这两种技术在同一个项目。

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

系统必备组件

视觉工作室 2013年视觉工作室表示 2013

或者,使用 NuGet 程序包管理器来安装所需的包。从 Visual Studio 中的工具菜单,选择库软件包管理器,然后选择程序包管理器控制台在程序包管理器控制台窗口中输入以下命令 ︰

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 版本控制

在此示例中,"/ 原料药,v1,产品"将被路由到不同的控制器比"/ 原料药,v2,产品"。

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

更多的信息,请参阅配置 Web API 与 ASP.NET 托管.

添加路由属性

这里是一个示例使用属性定义一条路线 ︰

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

字符串"客户 / {客户 id} / 订单"是用于路由的 URI 模板。Web API 会尝试匹配请求的 URI 模板。在此示例中,"客户"和"订单"是文字片段,和客户 id"{}"是一个可变的参数。以下 Uri 将匹配此模板 ︰

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

您可以限制通过使用约束,稍后在本主题中描述的匹配。

工艺路线模板中的客户 id"{}"参数匹配的方法中的客户 id参数名称的通知。当 Web API 调用控制器操作时,它试图将绑定路由参数。例如,如果 URI 是http://example.com/customers/1/orders,Web API 试图绑定到行动中的客户 id参数值"1"。

一个 URI 模板可以有几个参数 ︰

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

没有路由属性的任何控制器方法使用基于公约的路由。这种方式,你可以结合这两种类型的路由在同一个项目。

HTTP 方法

Web API 也选择基于 HTTP 方法 (GET、 邮政等) 请求的操作。默认情况下,Web API 查找开始时的控制器方法名称不区分大小写匹配。例如,一个名为PutCustomers的控制器方法匹配 HTTP 请求。

您可以重写本公约的装饰方法与任何下列属性 ︰

  • [] HttpDelete
  • [公共]
  • [] HttpHead
  • [] HttpOptions
  • [] HttpPatch
  • [] HttpPost
  • [] HttpPut

下面的示例将 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) { ... }
}

路由约束

路由约束让您限制匹配路由模板中的参数的方式。一般的语法是"{参数 ︰ 限制类型"。例如 ︰

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

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

在这里,如果 URI 的"id"部分是一个整数,只将选中的第一个路由。否则,将选择第二条路线。

下表列出了支持的约束。

约束 描述 示例
阿尔法 火柴大写或小写西文字母字符 (a-z、 A-Z) {x ︰ 阿尔法}
布尔值 匹配一个布尔值。 {x:} bool
日期时间 DateTime值匹配。 {x ︰ 日期时间}
十进制 匹配一个十进制值。 {x ︰ 十进制}
匹配一个 64 位的浮点值。 {x ︰ 双}
浮法 与 32 位浮点值匹配。 {x ︰ 浮}
guid 匹配一个 GUID 值。 {x:} guid
int 匹配一个 32 位整数值。 {x:} int
长度 匹配字符串与指定的长度或在一个指定的长度范围内。 {x:} length(6)
{x:} length(1,20)
匹配一个 64 位整数值。 {x ︰ 长}
最大 匹配,最大值的整数。 {x:} max(10)
maxlength 匹配字符串最大长度。 {x:} maxlength(10)
分钟 匹配的最小值的整数。 {x:} min(10)
minlength 匹配与最小长度的字符串。 {x:} minlength(10)
范围 匹配的值范围内的整数。 {x:} range(10,50)
正则表达式 匹配正则表达式。 {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?}") 中,1033年的默认值直接分配给方法的参数,所以该参数会有此确切的值。
  • 在第二个示例 ("{lcid = 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 时,它计算路线按特定的顺序。以指定的顺序,请将RouteOrder属性设置的路由属性上。首先计算较低的值。默认顺序值为零。

这里是如何确定的总排序 ︰

  1. 比较路由特性的RouteOrder属性。
  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. 订单/详细信息
  2. 订单 / {id}
  3. 订单 / {customerName}
  4. 订单 / {* 日期}
  5. 订单 / 悬而未决

请注意,"详细信息"是一个文字部分和出现之前"{id}",但是"待审"会出现最后因为RouteOrder属性是 1。(此示例假定那里没有客户命名"详细信息"或"挂起"。一般情况下,尽量避免含糊不清的路线。在此示例中, GetByCustomer为更好的路线模板是"客户 / {customerName}")

这篇文章的初衷是在 2014 年 1 月 20 日