前段時間一直忙於別的事情,終於搞定了繼續學習.NetCore。這次學習的主題是MVC中的路由。路由是所有MVC框架都會實現的一個組件,核心功能就是根據接收到的Http請求中的Path(對於http://localhost/Home/Index/12?test=555 來說,http是協議,localhost是域,Home/Index/12是Path,test=555是參數)部分,依次和路由規則集合中的規則進行匹配,匹配成功後由對應的Controller中的對應Action進行Http請求的處理。匹配不到則返回404錯誤。
大多數MVC框架路由規則的配置都大同小異,一般都是通過模板的方式來配置路由規則。有的還支持在Controller和Action上通過Attribute(Java中叫注解)進行更細粒度的配置。
.NetCore MVC支持通過全局的路由模板配置路由規則,也支持在Controller和Action上通過Attribute進行細粒度的路由配置。下面先說一下在Startup.cs中配置全局路由規則。
所謂的路由的模板就是一串字符串,當接收到Http請求後取出其中的Path部分,和模板進行對照,如果匹配模板則路由到對應的Controller和Action進行處理。我們可以在Startup.cs文件中的Configure方法中,添加MVC功能時進行路由配置,例如:
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller}/{action}/{id}"); });
其中name為路由規則的名稱,template為路由模板。這也引出了我們第一個概念,路由模板中的變量。
在模板"{controller }/{action }/{id}"中,用花括號括起來的是路由模板中的變量。例如其中變量的作用並不是必須在Path中匹配某個固定的字符串,而是起到一個占位的作用,例如上面的模板就可以匹配由“/”隔開的共三部分的Path,例如a/b/c可以匹配成功。而各個變量的值從Path中對應部分提取出來。例如
Home/Index/12可以匹配,其中controller為Home,action為Index,id為12
Home/Index則匹配失敗,因為只有2部分
Home/Index/12/34同樣匹配失敗,因為超過了3部分。
模板匹配成功後,會根據controller和action提取出的值路由:
Home/Index/12會路由到HomeController的Index方法,變量id為12
Test/Show/ab會路由到TestController的Show方法,變量id為ab
在Index或Show方法中,我們可以有兩種方法提取變量:
一種是在方法的參數列表中加入和變量相同名稱的參數,MVC會自動從變量列表中尋找並轉換為對應類型:
public IActionResult Index(string id, string controller, string action) { ViewData["Message"] = "id is " + id + ", controller is " + controller + ", action is " + action; return View(); }
另一種就是從RouteData中取出:
public IActionResult Index() { var controller = RouteData.Values["controller"].ToString(); var action = RouteData.Values["action"].ToString(); var id = RouteData.Values["id"].ToString(); ViewData["Message"] = "id is " + id + ", controller is " + controller + ", action is " + action; return View(); }
路由模板中的變量名稱是可以自己定義的,但controller和action(包括後面講的area)都是比較特殊的變量。其中controller提取出的值作為Controller的名稱,action提取出的值作為Controller中方法的名稱。為了讓每條路由規則都能夠路由到Controller和Action,在路由模板中都應該出現controller和action變量,但我們也可以給controller和action變量指定默認值,這樣在Path中省略了這部分時會用默認值代替。
由兩種方法可以配置變量的默認值:
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id=0}"); });
或者
routes.MapRoute( name: "default", template: "{controller}/{action}/{id}", defaults: new { controller = ”Home”, action = ”Index”, id = 0, });
這樣配置後Path中帶有默認值的部分可以省略,省略的規則和C#中帶默認值的參數一樣,例如:
空Path會被路由到HomeController,Index方法
Test會被路由到TestController,Index方法
Test/Show依然會被路由到TestController,Show方法
一般我會用第一種方法配置默認值,更加直觀和方便。但有時候有些需求是第一種方法難以做到的。例如我想給TestController的Show方法配置路由為TestShow,使用第一種方法可以這樣配置:”TestShow/{controller=Test}/{action=Show}”,這樣配置當Path為TestShow時的確可以路由到TestController的Show方法,但當Path為TestShow/Home/Index時會路由到HomeController的Index方法。
使用第二種方法配置:
routes.MapRoute( name: "test", template: "TestShow", defaults: new { controller = ”Test”, action = ”Show”, });
當Path為TestShow時可以路由到TestController的Show方法,但Path為Test/Home/Index則無法匹配模板。關於細粒度的路由配置更好的方法是給Test方法使用Route特性(Attribute)進行配置,後面會說到。
除了使用變量來配置路由模板,還可以使用靜態字符串。靜態字符串可以單獨使用,也可以與變量混合使用。
例如模板為:
”Durow/{controller}/{action}”
Durow/Home/About會路由到HomeController,About方法
Durow/Test/Show會路由到TestController,Show方法
也可以把靜態字符和變量混合起來,例如配置模板為:
”My{controller}/My{action}”
MyHome/MyAbout會被路由到HomeController,About方法
MyTest/MyShow會被路由到TestController,Show方法
除了通過給變量提供默認值使其可選外,也可以使用?把變量標記為可選。例如模板
“{controller}/{action}/{id?}”
其中id為可選變量,這樣配置後
Home/Index和Home/Index/12都會成功匹配。
如果一個模板需要匹配包含任意多個部分(Segments)的Path,可以使用*符號指定變量,使用*制定過的變量會把Path中匹配完成後剩余部分全部提取出來,例如模板:
”{controller}/{action}/{id?}/{*others}”
Home/Index/12/a/b/c/d,會路由到HomeController的Index方法,id為12,others為a/b/c/d
實際上僅從模板匹配的角度來說,上面的模板可以匹配所有的Path。唯一的問題就是匹配後對應的Controller和Action可能不存在。
實際應用中很可能會配置多條路由規則,當接收到Path時很可能不止一條規則能夠匹配。
最簡單的,我們配置以下兩條模板:
“{controller }/{action =About}”
“{controller }/{action =Index }”
當Path為Home時兩條路由都能匹配,那要怎麼選擇呢?其實很簡單粗暴,就是看哪條路由在前面。也就是說Path一旦成功匹配到模板後就會立即實施路由並忽略後面的模板。對於上面的配置來說Home會被路由到HomeController的About方法。所以在配置路由時一定要注意順序。
除了在Startup.cs中配置全局路由規則外,也可以針對單個Controller和其中的Action配置路由。方法就是在Controller類和Action方法上使用Route特性。例如在TestController的Show方法上使用Route特性:
[Route("TestShow")] public IActionResult Show() { return View(); }
當Path為TestShow時,即可路由到TestController的Show方法。
上面我們在介紹默認值時提到過,通過全局模板配置:
routes.MapRoute( name: "test", template: "TestShow", defaults: new { controller = ”Test”, action = ”Show”, });
也可以達到同樣的目的。不過區別在於,使用後一種方法時,如果還有”{controller}/{action}”這樣的模板,除了TestShow外,當Path為Test/Show可以匹配這個模板並路由到TestController的Show方法。而通過在Show方法上配置Route特性後,只有TestShow才可以路由,即使同時存在”{controller}/{action}”這樣的模板,Test/Show也無法路由。
第一次接觸用Route特性配置路由時,我很疑惑路由組件是如何把Path路由到對應的Controller和Action的,後來下了個斷點看了下RouteData對象,發現對於配置了路由的Action方法,其controller為方法所在的Controller的名稱,action為方法的名稱,而且在Route特性配置的路由模板中不能夠使用{controller}變量和{action}變量。這樣就保證了匹配模板的Path總能路由到這個Action。
對於在Controller類上配置的Route特性最終會分別配置到Controller中的每個Action上。例如我們在TestController上配置Route(“TestShow”),實際上就是給每個方法配置了Route(“TestShow"),所以當Path為TestShow時會報錯,提示有兩個action滿足匹配。那麼應當如何給Controller通過Route配置路由呢,可以使用[controller]和[action]。
對於[controller]和[action]我也不知道該怎麼叫,不能叫變量,功能上類似占位符。當我們在Controller類用Route特性配置路由時,如果使用了[controller]和[action],這樣當Route特性給Controller中每個Action配置路由時,[controller]會被替換為Controller名稱,[action]會被替換為Action名稱。舉個例子還是給TestController配置Route特性,配置為Route(“durow/[controller]/[action]”),這樣對於其中的Index方法來說,其路由模板為”durow/Test/Index”,controller為Test,action為Index。而對於Show方法來說路由模板為”durow/Test/Show”,controller為Test,action為Show。前面說過MVC會為每個Action創建一個ActionDescriptor對象存儲這個Action的路由信息。對於配置了Route特性的Action(再重復一下,給Controller類配置Route特性相當於給Controller中的每個Action配置Route特性),其ActionDescriptor中會有一個AttributeRouteInfo對象,對於未配置Route特性的Action,該對象為空。AttributeRouteInfo中包含了路由模板信息。
所以對於上面TestController的Route特性的配置,配置為Route(“durow/Test/[action]”)也能達到同樣的效果。不過使用Route(“durow/[controller]/[action]”)語義更強更通用。
在Route特性中配置模板也是可以使用變量的,同樣可以使用?標記變量可選。例如可以給TestController配置Route(“durow/[controller]/[action]/{id?}”)。但需要注意的是Route特性的模板中變量不能使用默認值(包括[controller]和[action]),也不能使用*提取Path所有剩余部分。
啰啰嗦嗦居然寫了這麼多,其實實際使用中很可能用不到多麼復雜的路由,一般一條通用規則,一條Area相關的規則就可以了。不過詳細了解了路由規則,當以後遇到有些奇葩的特殊需求時能夠有更加開闊的思路。後面講討論一下路由模板中的約束和自定義約束。再後面討論一下使用Areas。