原文:http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx

作者: 翻译:汪宇杰

路由(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}")]

总之配置是很自由的,只有想不到,没有做不到。