每個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