ASP.NET MVC5高级编程 之 路由
每个ASP.NET MVC应用程序都需要路由来定义自己处理请求的方式。路由是MVC应用程序的入口点。路由的核心工作是将一个请求映射到一个操作
路由主要有两种用途:
- 匹配传入的请求(该请求不匹配服务器文件系统中的文件),并把这些请求映射到控制器操作。
- 构造传出的URL,用来响应控制器操作
1.特性路由
1.1 路由URL
创建一个ASP.NET MVC Web应用程序项目后,浏览Global.asax.cs文件中的代码中,Application_Start方法中调用了一个名为RegisterRoutes的方法。该方法是集中控制路由的地方,包含在~/App_Start/RouteConfig.cs文件中。
1 public static void RegisterRoutes(RouteCollection routes)
2 {
3 routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
4
5 routes.MapRoute(
6 name: "Default",
7 url: "{controller}/{action}/{id}",
8 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
9 );12 }
修改RegisterRoutes方法中的内容,只通过调用MapMvcAttributeRoutes注册方式让RegisterRoutes方法启用特性路由。修改后的方法如下:
1 public static void RegisterRoutes(RouteCollection routes)
2 {
3 routes.MapMvcAttributeRoutes();
4 }
路由的核心工作是将一个请求映射到一个操作。完成这项工作最简单的方法是在一个操作方法上直接使用一个特性:
//响应URL为 /about的请求
1 public class HomeController : Controller
2 {
3 [Route("about")]
4 public ActionResult About()
5 {
6 return View();
7 }
8 }
每当收到URL为/about的请求时,这个路由特性就会运行About方法。MVC收到URL,然后运行代码。
如果对于操作有多个URL,就可以使用多个路由特性。例如,想让首页可以通过/、/home和/home/index这几个URL都能访问,可以设置路由如下:
//响应URL为 /、/home和/home/index三个URL
1 [Route("")]
2 [Route("home")]
3 [Route("home/index")]
4 public ActionResult Index()
5 {
6 return View();
7 }
传入路由特性的字符串叫做路由模版,他就是一个模式匹配规则,决定了这个路由是否是用于传入的请求。如果匹配,MVC就运行路由的操作方法。
1.2 路由值
对于简单的路由,适合刚才的静态路由,但并不是每个URL都是静态的。例如,如果操作显示个人记录的详情,则需要在URL中包含记录的ID。通过添加路由参数可解决这个问题:
//id作为一个动态参数
1 [Route("Person/{id}")]
2 public ActionResult Details(int id)
3 {
4 return View();
5 }
通过花括号的id,就可以作为一个占位符。
多个占位符的情况可如下标识:
//具有多个占位符
1 [Route("{year}/{month}/{day}")]
2 public ActionResult Index(string year, string month, string day)
3 {
4 return View();
5 }
1.3 控制器路由
之前的讨论了如何把路由特性直接添加到操作方法上,但是很多时候,控制器类中的方法遵循的模式具有相似的路由模版,以HomeController控制器为例:
1 public class HomeController : Controller
2 {
3 [Route("home/index")]
4 public ActionResult Index()
5 {
6 return View();
7 }
8 [Route("home/about")]
9 public ActionResult About()
10 {
11 return View();
12 }
13 [Route("home/contact")]
14 public ActionResult Contact()
15 {
16 return View();
17 }
18 }
除了URL的最后一段,这些路由是相同的。所以期望能有一个方法能映射到home下的一个URL。
1 [Route("home/{action}")]
2 public class HomeController : Controller
3 {
4 public ActionResult Index()
5 {
6 return View();
7 }
8 public ActionResult About()
9 {
10 return View();
11 }
12 public ActionResult Contact()
13 {
14 return View();
15 }
16 }
使用控制器类的一个特性代替每个方法上的所有路由特性。在控制器类上定义路由时,可以使用一个叫做action的特殊路由参数,它可以作为任意操作名称的占位符。action参数的作用相当于每个操作方法上单独添加路由,并静态输入操作名:它只是一种更加方便的语法而已。
有时控制器上的某些具有与其他操作稍微不同的路由。此时,我们可以把最通用的路由放到控制器上,然后在具有不同路由模式的操作上重写默认路由。例如,如果我们认为/home/index过于冗长,但是又想支持/home,就可以如下:
1 [Route("home/{action}")]
2 public class HomeController : Controller
3 {
4 [Route("home")]
5 [Route("home/index")]
6 public ActionResult Index()
7 {
8 return View();
9 }
10 public ActionResult About()
11 {
12 return View();
13 }
14 public ActionResult Contact()
15 {
16 return View();
17 }
18 }
在操作方法级别指定路由特性时,会覆盖控制器级别指定的任何路由特性。在前面的例子中,如果Index方法只有第一个路由特性(home),那么尽管控制器有一个默认路由
home/{action},也不能通过home/index来访问Index方法。如果需要定义某个操作的路由,并且仍希望应用默认的控制器路由,就需要在操作上再次列出控制器的路由。
前面的类仍然带有重复性。每个路由都以home/开头(毕竟,类的名称是HomeController)。通过使用RoutePrefix,可以仅在一个地方指定路由以home/开头:
1 [RoutePrefix("home")]
2 [Route("{action}")]
3 public class HomeController : Controller
4 {
5 [Route("")]
6 [Route("index")]
7 public ActionResult Index()
8 {
9 return View();
10 }
11 public ActionResult About()
12 {
13 return View();
14 }
15 public ActionResult Contact()
16 {
17 return View();
18 }
19 }
现在,所有的路由特性都可以省略home/,因为前缀会自动加上home/。这个前缀只是一个默认值,必要时可以覆盖该行为。例如,除了支持/home和/home/index以外,我们还想让HomeController支持/。为此,使用~/作为路由模版的开头,路由前缀就会被忽略。
在下面的代码中,HomeController的Index方法支持全部三种URL(/、/home和/home/index):
//支持URL为 /、/home和/home/index
1 [RoutePrefix("home")]
2 [Route("{action}")]
3 public class HomeController : Controller
4 {
5 [Route("~/")]
6 [Route("")] //此处也可以简写 [Route]
7 [Route("index")]
8 public ActionResult Index()
9 {
10 return View();
11 }
12 public ActionResult About()
13 {
14 return View();
15 }
16 public ActionResult Contact()
17 {
18 return View();
19 }
20 }
1.4 路由约束
因为方法参数的名称正好位于由路由特性及路由参数名称的下方,所以很容易忽视这两种参数的区别。
1 [Route("person/{id}")]
2 public ActionResult Details(int id)
3 {
4 return View();
5 }
对于这种情况,当收到/person/bob这个URL的请求时,根据路由规则,会将bob作为id参数传入,但bob无法转换为int类型,所以方法不能执行。
如果想同时支持/person/bob和/person/1,并且每个URL运行不同的操作,可以尝试添加具有不同特性路由的方法重载,如下所示:
1 [Route("person/{id:int}")]
2 public ActionResult Details(int id)
3 {
4 return View();
5 }
6 [Route("person/{name}")]
7 public ActionResult Details(string name)
8 {
9 return View();
10 }
因为传入的参数存在二义性,1也可以解释为字符串,因此需要添加int约束。路由约束是一种条件,只有满足该条件时,路由才能匹配。这种约束叫做内联约束。
内联路由约束为控制路由何时匹配提供了精细的控制。如果URL看上去相似,但是具有不同的行为,就可以使用路有约束来表达这些URL之间的区别,并把它们映射到正确的操作。
1.5 路由的默认值
1 [Route("home/{action}")]
2 public class HomeController : Controller
3 {
4 public Action Index()
5 {
6 return View();
7 }
8 }
对于以上代码,如果通过URL为 : /home进行访问,根据类定义的路由模版home/{action},以上代码不能运行。因为定义的路由只匹配包含两个段的URL,但是/home只包含一个段。
如果我们想让Index成为默认的action,路由API允许为参数提供默认值,代码如下:
[Route("home/{action=Index}")]
{action=Index}这段代码为{action}参数定义了默认值。此时,该默认情况就允许路由匹配没有action参数的请求。也就是现在既可以匹配具有一个段的URL,也可以匹配具有两个段的URL。
[Route("home/{action=Index}/{id?}")]
这段代码提供默认值Index,以及可选值id。
因为第二个段id是可选值,因此匹配的URL不再必须包含两个段。
2.传统路由
在~/App_Start/RouteConfig.cs文件中存在方法RegisterRoutes,在方法中添加传统路由,代码如下:
1 public static void RegisterRoutes(RouteCollection routes)
2 {
3 routes.MapRoute("simple", "{first}/{second}/{third}");
4 }
MapRoute方法的最简单形式是采用路由名称和路由模版。
与特性路由一样,路由模版是一种模式匹配规则,用来决定该路由是否应该处理传入的请求(基于请求的URL决定)。特性路由与传统路由之间最大的区别在于如何将路由链接到操作方法。传统路由依赖于名称字符串而不是特性来完成这种链接。
在操作方法上使用特性路由时,不需要任何参数,路由就可以工作。路由特性被直接放到了操作方法上,当路由匹配时,MVC知道去运行该操作方法。将特性路由放到控制器类上时,MVC知道使用哪个类(因为该类上有路由特性),但是不知道运行哪个方法,所以我们使用特殊的action参数来通过名称指明要运行的方法。
如果针对上面的简单路由请求一个URL(例如a/b/c),回收到一个500错误。因为传统路由不会自动链接控制器或操作。要指定操作,需要使用action参数(就像在控制器类上使用路由特性时所做的那样)。要指定控制器,需要使用一个新参数controller。如果不定义这些参数,MVC不会知道想要运行的操作方法,所以会通过返回一个500错误。
通过修改简单路由,使其包含这些必需参数,可以解决这个问题:
routes.MapRoute("simple","{controller}/{action}");
现在,如果请求一个URL,如/home/index,MVC会认为这是在请求一个名为home的{controller}和一个名为index的{action}。根据约定,MVC会把后缀Controller添加到{controller}路由参数的值上,并尝试定位具有该名称(区分大小写)并实现了System.Web.Mvc.IController接口的类型。
注:特性路由直接绑定到方法和控制器,而不是仅指定名称,这意味着他们更加精确。例如,使用特性路由时,可以随意命名控制器类,只要以Controller后缀结尾即可(名称不需要与URL相关)。在操作方法上直接使用特性,意味着MVC知道运行哪个重载版本,并不需要在同名的多个操作方法中选择。
2.1 路由值
controller和action参数很特殊,因为他们映射到控制器和操作的名称,是必须参数。但是这两个参数并不是可以使用的全部参数。更新路由来包含第三个参数:
routes.MapRoute("simple", "{controller}/{action}/{id}");
对于 /albums/display/123的请求,会导致实例化MVC的类,调用其中的Display方法,同时将123传递给Display方法的参数id。
routes.MapRoute("simple", "site/{controller}/{action}/{id}");
上面的路由指出请求URL的第一段只有以site开头,才能与请求相匹配。因此,上面的路由可以匹配/site/albums/display/123,而不能匹配/albums/display/123。
此外,还有更灵活的路由语法规则:在路径段中允许字面值和路由参数混合在一起。它仅有的限制就是不允许有两个连续的路由参数
1 //有效
2 {language}-{country}/{controller}/{action}
3 {controller}.{action}.{id}
4 //无效
5 {controller}{action}/{id}
只需要记住,除非路由提供了controller和action参数,否则MVC不知道URL运行哪些代码。
2.2 路由默认值
特性路由通过将参数{id}内联修改为{id?},使得id成为可选的参数。传统路由则是将信息放到路由模版后面的单独一个参数中:
1 routes.MapRoute("simple", "{controller}/{action}/{id}",
2 new {id=UrlPatameter.Optional});
第三个参数用于默认值{id=UrlPatameter.Optional},这段代码为{id}参数定义了默认值。
下面的代码为action指明默认值:
1 routes.MapRoute("simple",
2 "{controller}/{action}/{id}",
3 new {id=UrlPatameter.Optional, action = "index"});
2.3 路由约束
传统路由允许使用正则表达式来限制路由是否匹配请求。而在特性路由中,使用类似于{id:int}的语法在路由模版中内联指定约束,具体代码如下:
1 routes.MapRoute("blog", "{year}/{month}/{day}",
2 new {controller = "blog", action = "index"},
3 //路由约束
4 new {year=@"\d{4}", month=@"\d{2}", day=@"\d{2}"});
注:路由机制会自动的使用"^"和“$”符号包装指定的约束表达式。换言之,此处可以匹配参数“1234”,但不匹配“adc1234def”,也不能匹配“/08/05/25”。
3.选择特性路由还是传统路由
选择传统路由:
- 想要集中配置所有路由
- 使用自定义约束对象
- 存在现有可工作的应用对象,而又不想修改应用程序
选择特性路由:
- 想把路由与操作代码保存在一起
- 创建新应用程序,或者对现有应用程序进行巨大修改
传统路由的集中配置意味着可以在一个地方理解请求如何映射到操作。传统路由也比特性路由更灵活。例如,向传统路由添加自定义约束对象很容易。C#中的特性只支持特定类型的参数,对于特性路由,这意味着只能在路由模版字符串中指定约束。
特性路由很好的把关于控制器的所有内容放到了一起,包括控制器使用的URL和运行的操作。
4.URL生成详解
- 开发人员调用像Html.ActionLink或Url.Action之类的方法,这些方法反过来再调用RouteCollection.GetVirtualPath方法,并向它传递一个ResponseContext对象、一个包含值的字典以及用来选择生成URL的路由名称(可选参数)。
- 路由机制查看要求的路由参数(即没有提供路由参数对默认值),并确保提供的路由值字典为每一个要求的参数提供一个值。否则URL生成程序会立即停止,并返回空值。
- 一些路有可能包含没有对应路由参数的默认值。例如,路有可能为category键提供默认值“pastries”,但是category不是路有URL的一个参数。这种情况下,如果用户传入的路由值字典为category提供了一个值,那么该值必须匹配category的默认值。
- 然后路由系统应用路有的约束,如果有的话。
- 路由匹配成功!现在可以通过查看每一个路由参数,并尝试利用字典中的对应值填充相应参数,进而生成URL