原文:http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx
作者:Ken Egozi 翻译:汪宇杰
路由(Routing)指的是ASP.NET MVC如何将一个URL匹配到一个Action上的过程。MVC5(译者注:MVC框架的最新版本,与VS2013一起发布)支持一种新的路由类型,叫做基于特性的路由(attribute routing),正如它的名称所隐含的,特性路由使用特性(译者注:C#的特性标记,如[Serializable])定义路由。特性路由给了你更多的自由去控制Web应用中的URL地址。
早期风格的路由,即基于契约的路由,仍然被支持。事实上,你可以把这两者结合起来。
这篇文章涵盖ASP.NET MVC5特性路由的基本玩法。
为毛用特性路由?
比如,一个社交增强的电子商务系统可以定义以下路由:
{productId:int}/{productTitle}
映射到ProductsController.Show(int id)
{username}
映射到ProfilesController.Show(string username)
{username}/catalogs/{catalogId:int}/{catalogTitle}
映射到CatalogsController.Show(string username, int catalogId)
不要在意语法的细节,稍候我们会讨论。
在以前版本的ASP.NET MVC里,这些规则会被定义在RouteConfig.cs文件里,指向实际的controller和action,就像这样:
routes.MapRoute( name: "ProductPage", url: "{productId}/{productTitle}", defaults: new { controller = "Products", action = "Show" }, constraints: new { productId = "\\d+" } );
当路由规则的定义和action如此相关的时候,与其将它分开定义在别的文件里,不如定义在同一份源代码文件中,这样对于URL和action的关系看起来会更加合理。之前我们定义的路由可以简单的用特性标记写成这样:
[Route("{productId:int}/{productTitle}")] public ActionResult Show(int productId) { ... }
启用特性路由
要启用特性路由,只要在配置文件中调用MapMvcAttributeRoutes方法
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapMvcAttributeRoutes(); } }
你也可以把特性路由和基于契约的路由撸到一起
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapMvcAttributeRoutes(); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
可选的URL参数和默认值
在路由参数后面加个问号就可以把它撸成可选的。你也可以在参数后面加个等号指定默认值。
public class BooksController : Controller { // eg: /books // eg: /books/1430210079 [Route("books/{isbn?}")] public ActionResult View(string isbn) { if (!String.IsNullOrEmpty(isbn)) { return View("OneBook", GetBook(isbn)); } return View("AllBooks", GetBooks()); } // eg: /books/lang // eg: /books/lang/en // eg: /books/lang/he [Route("books/lang/{lang=en}")] public ActionResult ViewByLanguage(string lang) { return View("OneBook", GetBooksByLanguage(lang)); } }
在这个例子里面,/books和/books/1430210079都会被路由到View这个action上,前者会列出书籍列表,后者会显示一本书的详细信息。/books/lang和/books/lang/en也是一样的。
路由前缀
通常,在同一个controller下的路由都有相同的前缀,比如
public class ReviewsController : Controller { // eg: /reviews [Route("reviews")] public ActionResult Index() { ... } // eg: /reviews/5 [Route("reviews/{reviewId}")] public ActionResult Show(int reviewId) { ... } // eg: /reviews/5/edit [Route("reviews/{reviewId}/edit")] public ActionResult Edit(int reviewId) { ... } }
这时候你可以给整个controller加上一个[RoutePrefix]特性
[RoutePrefix("reviews")] public class ReviewsController : Controller { // eg.: /reviews [Route] public ActionResult Index() { ... } // eg.: /reviews/5 [Route("{reviewId}")] public ActionResult Show(int reviewId) { ... } // eg.: /reviews/5/edit [Route("{reviewId}/edit")] public ActionResult Edit(int reviewId) { ... } }
如果需要重写路由规则,就加个波浪线(~)
[RoutePrefix("reviews")] public class ReviewsController : Controller { // eg.: /spotlight-review [Route("~/spotlight-review")] public ActionResult ShowSpotlight() { ... } ... }
默认路由
你也可以在controller级别定义Route特性,将action作为一个参数。这个路由会被应用到controller上的所有action,除非某个action已经定义了一个[Route]特性,重写controller的默认行为。
[RoutePrefix("promotions")] [Route("{action=index}")] public class ReviewsController : Controller { // eg.: /promotions public ActionResult Index() { ... } // eg.: /promotions/archive public ActionResult Archive() { ... } // eg.: /promotions/new public ActionResult New() { ... } // eg.: /promotions/edit/5 [Route("edit/{promoId:int}")] public ActionResult Edit(int promoId) { ... } }
路由约束
路由约束让你可以限制参数在路由模板里如何匹配。通常的语法是{parameter:constraint},例如:
// eg: /users/5 [Route("users/{id:int}"] public ActionResult GetUserById(int id) { ... } // eg: users/ken [Route("users/{name}"] public ActionResult GetUserByName(string name) { ... }
这里,如果id是int类型的值,第一个路由就会匹配成功,其他类型则会匹配到第二个路由上去。
下面这个表格列出了所有支持的约束类型。
约束 | 描述 | 示例 |
---|---|---|
alpha | 匹配大小写字母 (a-z, A-Z) | {x:alpha} |
bool | 匹配布尔值 | {x:bool} |
datetime | 匹配DateTime(时间和日期)类型 | {x:datetime} |
decimal | 匹配decimal类型 | {x:decimal} |
double | 匹配64bit浮点数 | {x:double} |
float | 匹配32bit浮点数 | {x:float} |
guid | 匹配GUID | {x:guid} |
int | 匹配32bit整数 | {x:int} |
length | 匹配定长字符串 | {x:length(6)} {x:length(1,20)} |
long | 匹配64bit整数 | {x:long} |
max | 匹配最大为n的整数 | {x:max(10)} |
maxlength | 匹配至多n个字符的字符串 | {x:maxlength(10)} |
min | 匹配最小为n的整数 | {x:min(10)} |
minlength | 匹配至少n个字符的字符串 | {x:minlength(10)} |
range | 匹配整数区间 | {x:range(10,50)} |
regex | 匹配正则表达式 | {x:(^\d{3}-\d{3}-\d{4}$)} |
注意,其中一些约束要求用括号接受参数。
你可以用分号分隔,定义多个约束,如:
// eg: /users/5 // but not /users/10000000000 because it is larger than int.MaxValue, // and not /users/0 because of the min(1) constraint. [Route("users/{id:int:min(1)}")] public ActionResult GetUserById(int id) { ... }
自定义路由约束
你可以实现IRouteConstraint接口以定义自己的约束,如
public class ValuesConstraint : IRouteConstraint { private readonly string[] validOptions; public ValuesConstraint(string options) { validOptions = options.Split('|'); } public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { object value; if (values.TryGetValue(parameterName, out value) && value != null) { return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase); } return false; } }
下面代码是如何去注册这个路由
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add("values", typeof(ValuesConstraint)); routes.MapMvcAttributeRoutes(constraintsResolver); } }
现在你可以在你的路由中应用这个规则
public class TemperatureController : Controller { // eg: temp/celsius and /temp/fahrenheit but not /temp/kelvin [Route("temp/{scale:values(celsius|fahrenheit)}")] public ActionResult Show(string scale) { return Content("scale is " + scale); } }
路由名称
你可以给一个路由指定一个名字,以便于URL生成,就像这样:
[Route("menu", Name = "mainmenu")] public ActionResult MainMenu() { ... }
你可以用Url.RouteUrl生成一个超链接
<a href="@Url.RouteUrl("mainmenu")">Main menu</a>
区域
你可以用[RouteArea]特性指明某个controller属于某个区域(area),这样你就可以很安全的撸掉AreaRegistration类里面那个区域的注册代码了。
(译者注:下面这段代码原文排版有误,应该在“路由名称”一节)
[RoutePrefix("menu")] [Route("{action}")] public class MenuController : Controller { // eg: /admin/menu/login public ActionResult Login() { ... } // eg: /admin/menu/show-options [Route("show-options")] public ActionResult Options() { ... } // eg: /stats [Route("~/stats")] public ActionResult Stats() { ... } }
使用这样的controller,下面的链接会生成字符串"/Admin/menu/show-options"
Url.Action("Options", "Menu", new { Area = "Admin" })
你可以用AreaPrefix特性为这个area指定前缀
[RouteArea("BackOffice", AreaPrefix = "back-office")]
如果你同时使用基于特性的area路由,和传统的基于契约的area路由,你就必须得确保area的注册过程在特性(attribute)被配置之后,然而又要在默认的基于契约的路由定义之前。原因是路由的注册是先从最精确的(基于特性的)再到普通的(区域注册)再到最一般的(默认路由),以确保最一般的规则不会在管道上(译者注:IIS管道?)过早的匹配请求时把高层次(精确的)路由规则给“隐藏”起来。
例如
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapMvcAttributeRoutes(); AreaRegistration.RegisterAllAreas(); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); }
译者补充:
MVC5的这个特性来源于开源的力量,在微软把MVC开源之后,出现了一个三方的库 http://attributerouting.net/。在MVC5以前要做attribute routing一直用的都是这个库,直到MVC5里面,微软把他的代码直接撸进了自己的产品。
和原版本比起来,ASP.NET MVC5的attribute routing定义更加简洁。不过上面这篇文章里只是涵盖了基本的应用,更细致的文档目前微软官方还没有撸出来,根据我的使用经验,多数都是和attributerouting这个库是一样的。
比如你可以在一个action上定义两个attribute routing,这个例子是我博客系统代码里的:
[Route("Archive/{year:regex(\\d{4})}")] [Route("Archive/{year:regex(\\d{4})}/{month:regex(\\d{1,2})}")] public ActionResult GetArchiveByTime(string year, string month)
另外一些复杂的,像博客系统的slug,用年月日分隔的,可以用正则表达式完成
[Route("Post/{year:regex(\\d{4})}/{month:regex(\\d{1,2})}/{day:regex(\\d{1,2})}/{slug}")]
总之配置是很自由的,只有想不到,没有做不到。
不错.希望能看到更多的MVC文章!