导航页面
http://blog.csdn.net/wf824284257/article/details/79475115
上一步
ASP.NET Web API 中的路由以及Action的选择
http://blog.csdn.net/wf824284257/article/details/79491961
开始
路由是指 Web API 将URI匹配到一个action。 Web API 2 支持一种新的路由方式,叫做“特性路由”。就像它的名字所示,特性路由使用特性来定义路由规则。特性路由可以让你更好的控制 Web API 的 URI。 比如,你可以很容易的创建指向层次性资源的URI。
早些时候的路由规则,被称为基于惯例的路由,现在仍被广泛支持。事实上,你可以在同一个项目中同时使用这两种技术。
这篇教程讲了如何使用特性路由,并描述了特性路由的多种多样的使用方法。这里有一篇端到端的使用了特性路由的教程,可以了解一下:https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/create-a-rest-api-with-attribute-routing
前提
本教程使用VS2017。
当然,也可以使用 NuGet Package Manager(NyGet程序包管理器)来安装必要的包。“菜单”-> “工具” -> “NuGet Package Manager”(Nuget程序包管理器) -> Package Manager Console(程序包管理器控制台).在PM Console里面输入以下命令:
Install-Package Microsoft.AspNet.WebApi.WebHost
为什么使用特性路由
第一个被发布的Web API 使用的是基于惯例的路由。在那种路由方式中,我们需要定义一个或多个路由模板(路由模板基本就是参数化的字符串)。当framework收到一个请求时,它使用路由模板来匹配请求的URI。(要了解更多基于惯例的路由,可以看这篇教程:https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api )。
基于惯例的路由规则的一个优点是,路由模板被定义在单一的地方,并且路由规则适用于所有的控制器。不幸运的是,基于惯例的路由规则很难支持某些在RESTful API中很常见的URI. 例如,资源地址经常包含了子资源地址:顾客有订单、电影有演员,书有作者之类的。我们创建URI经常需要来反应出这些关系。
/customers/1/orders
对于这样的URI,使用基于惯例的路由规则是很难创建出来的。尽管可以做,但是如果有非常多的controller和资源类型的话,这样的路由规则往往很难通配。
使用特性路由,为这样的URI定义一个路由规则是非常简单的。只需要在这个controller的action上加这样一条特性即可:
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
这里有一些其他的路由模式,在这些模式下,使用特性路由可以更简单的做到。
API版本
在这里例子中, “/api/v1/products”与”/api/v2/products”将会被路由到不同的控制器。
/api/v1/products
/api/v2/products
多参数型
在这里例子中,“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 的配置,可以看这篇教程:https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/configuring-aspnet-web-api
Note: Migrating From 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);
}
注意:要了解更多,可以看这篇教程:https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/configuring-aspnet-web-api#webhost
添加路由特性
这里有一个使用了特性的路由规则的例子:
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
你可以使用 constraints 来限制匹配,接下来会讲到。
要注意,这个方法中的路由模板中的”{customerId}”参数 与 方法中的customerId参数匹配。当 Web API 调用这个controller的这个action时,它会尝试绑定路由参数。例如,如果URI是 http://example.com/customers/1/orders, Web API 会尝试将“1”绑定到action的customerId参数。
一个URI模板可以有好几个参数:
[Route("customers/{customerId}/orders/{orderId}")]
public Order GetOrderByCustomer(int customerId, int orderId) { ... }
任何没有特性路由的控制器方法都会使用基于惯例的路由规则。也就是说,你可以在同一个项目中结合使用这两种路由规则。
HTTP方法
Web API 也通过请求的HTTP方法来选择action。默认情况下,Web API 会通过大小写不敏感的匹配方式来选择一个以对应的HTTP方法开头的action。比如,一个controller的方法名为PutCustomers,则它匹配HTTP PUT 请求。
你可以通过使用以下特性来使这样的惯例规则不再适用。
[HttpDelete]
[HttpGet]
[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() { }
路由前缀
通常情况下,同一个controller中的路由路径是有相同前缀的,例如:
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特性来修饰一个controller来为它的路由添加公共前缀:
[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段是整形时才会被选择。否则,第二个路由将会被选择。
下面这个表列出了被支持的约束:
主要到有些约束,比如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参数以及默认值
你可以通过在路由模板中的参数后面加一个问号来使该参数成为一个可选参数。如果一个路由参数是可选参数,你必须为它指定一个默认值。
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直接被指定在方法的参数中,所以该参数将会获取到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;
}
}
路由顺序
当framework尝试将URI匹配到路由规则时,它会使用特定的顺序来评价路由。为了指定顺序,要在特性路由中设置RouteOrder属性,该属性值越小的路由将会越先被评价。该属性默认值为0。
以下是定义全部顺序的步骤:
- 在特性路由中比较RouteOrder 的值。
- 查看每个路由模板的URI段。对于每个URI段,按以下规则排序
a. 字面量
b. 带有约束的路由参数
c. 不带有约束的路由参数
d. 带有约束的通配符参数
e. 不带有约束的通配符参数 - 如果有评价结果相等的情况,将会按照路由模板的大小写不敏感的字符串比较来排序。
下面是一个例子。假设你定义了以下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) { ... }
}
这些路由将按照以下排序:
- orders/details
- orders/{id}
- orders/{customerName}
- orders/{*date}
- orders/pending
注意到“details”是一个出现在”{id}”之前的字面量段,但是”pending” 是在最后的,因为它的RouteOrder 值为1。(这个例子假设没有客户名字是”details” 或 “pending”。一般情况下,要避免定义有歧义的路由规则。在这个例子中,对于 GetByCustomer 来说更好的路由模板是”customers/{customerName}” )
下一步
结束
本文为微软官方文档的个人译文。本译文的非商用转载请注明地址及作者,感谢。禁止商用转载。若经发现,将依法追究责任。
英文原文地址:https://docs.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
原文作者: MikeWasson and Other 5 Contributors
主作者链接:https://github.com/MikeWasson
作者尊重微软公司的知识产权,若贵公司认为该博客有损贵公司利益,请联系作者删除
译者:大吴凡 http://dawufan.cn