0x00 路由在MVC中起到的作用

前段時間一直忙于別的事情,終于搞定了繼續(xù)學(xué)習(xí).NetCore。這次學(xué)習(xí)的主題是MVC中的路由。路由是所有MVC框架都會實(shí)現(xiàn)的一個組件,核心功能就是根據(jù)接收到的Http請求中的Path(對于http://localhost/Home/Index/12?test=555 來說,http是協(xié)議,localhost是域,Home/Index/12是Path,test=555是參數(shù))部分,依次和路由規(guī)則集合中的規(guī)則進(jìn)行匹配,匹配成功后由對應(yīng)的Controller中的對應(yīng)Action進(jìn)行Http請求的處理。匹配不到則返回404錯誤。

 

大多數(shù)MVC框架路由規(guī)則的配置都大同小異,一般都是通過模板的方式來配置路由規(guī)則。有的還支持在Controller和Action上通過Attribute(Java中叫注解)進(jìn)行更細(xì)粒度的配置。

.NetCore MVC支持通過全局的路由模板配置路由規(guī)則,也支持在Controller和Action上通過Attribute進(jìn)行細(xì)粒度的路由配置。下面先說一下在Startup.cs中配置全局路由規(guī)則。

0x01 在Startup.cs中配置路由

所謂的路由的模板就是一串字符串,當(dāng)接收到Http請求后取出其中的Path部分,和模板進(jìn)行對照,如果匹配模板則路由到對應(yīng)的Controller和Action進(jìn)行處理。我們可以在Startup.cs文件中的Configure方法中,添加MVC功能時進(jìn)行路由配置,例如:

app.UseMvc(routes => {
    routes.MapRoute(
        name: "default",
        template: "{controller}/{action}/{id}");
});

其中name為路由規(guī)則的名稱,template為路由模板。這也引出了我們第一個概念,路由模板中的變量。

1.路由模板中的變量

在模板"{controller }/{action }/{id}"中,用花括號括起來的是路由模板中的變量。例如其中變量的作用并不是必須在Path中匹配某個固定的字符串,而是起到一個占位的作用,例如上面的模板就可以匹配由“/”隔開的共三部分的Path,例如a/b/c可以匹配成功。而各個變量的值從Path中對應(yīng)部分提取出來。例如

Home/Index/12可以匹配,其中controller為Home,action為Index,id為12

Home/Index則匹配失敗,因?yàn)橹挥?部分

Home/Index/12/34同樣匹配失敗,因?yàn)槌^了3部分。

模板匹配成功后,會根據(jù)controller和action提取出的值路由:

Home/Index/12會路由到HomeController的Index方法,變量id為12

Test/Show/ab會路由到TestController的Show方法,變量id為ab

2.變量值得獲取

在Index或Show方法中,我們可以有兩種方法提取變量:

一種是在方法的參數(shù)列表中加入和變量相同名稱的參數(shù),MVC會自動從變量列表中尋找并轉(zhuǎn)換為對應(yīng)類型:

public IActionResult Index(string id, string controller, string action)
{
    ViewData["Message"] = "id is " + id + ",  controller is " + controller + ",  action is " + action; return View();
}

另一種就是從RouteData中取出:

復(fù)制代碼
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();
}
復(fù)制代碼

路由模板中的變量名稱是可以自己定義的,但controller和action(包括后面講的area)都是比較特殊的變量。其中controller提取出的值作為Controller的名稱,action提取出的值作為Controller中方法的名稱。為了讓每條路由規(guī)則都能夠路由到Controller和Action,在路由模板中都應(yīng)該出現(xiàn)controller和action變量,但我們也可以給controller和action變量指定默認(rèn)值,這樣在Path中省略了這部分時會用默認(rèn)值代替。

3.變量的默認(rèn)值

由兩種方法可以配置變量的默認(rèn)值:

app.UseMvc(routes => {
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id=0}");
});

或者

復(fù)制代碼
routes.MapRoute(
    name: "default",
    template: "{controller}/{action}/{id}",
    defaults: new {
        controller = ”Home”,
        action = ”Index”,
        id = 0,
    });
復(fù)制代碼

這樣配置后Path中帶有默認(rèn)值的部分可以省略,省略的規(guī)則和C#中帶默認(rèn)值的參數(shù)一樣,例如:

空Path會被路由到HomeController,Index方法

Test會被路由到TestController,Index方法

Test/Show依然會被路由到TestController,Show方法

一般我會用第一種方法配置默認(rèn)值,更加直觀和方便。但有時候有些需求是第一種方法難以做到的。例如我想給TestController的Show方法配置路由為TestShow,使用第一種方法可以這樣配置:”TestShow/{controller=Test}/{action=Show}”,這樣配置當(dāng)Path為TestShow時的確可以路由到TestController的Show方法,但當(dāng)Path為TestShow/Home/Index時會路由到HomeController的Index方法。

使用第二種方法配置:

復(fù)制代碼
routes.MapRoute(
    name: "test",
    template: "TestShow",
    defaults: new {
        controller = ”Test”,
        action = ”Show”, 
    });
復(fù)制代碼

當(dāng)Path為TestShow時可以路由到TestController的Show方法,但Path為Test/Home/Index則無法匹配模板。關(guān)于細(xì)粒度的路由配置更好的方法是給Test方法使用Route特性(Attribute)進(jìn)行配置,后面會說到。

4.路由規(guī)則中的靜態(tài)字符

除了使用變量來配置路由模板,還可以使用靜態(tài)字符串。靜態(tài)字符串可以單獨(dú)使用,也可以與變量混合使用。

例如模板為:

”Durow/{controller}/{action}”

Durow/Home/About會路由到HomeController,About方法

Durow/Test/Show會路由到TestController,Show方法

也可以把靜態(tài)字符和變量混合起來,例如配置模板為:

”My{controller}/My{action}”

MyHome/MyAbout會被路由到HomeController,About方法

MyTest/MyShow會被路由到TestController,Show方法

5.使用?標(biāo)記變量可選

除了通過給變量提供默認(rèn)值使其可選外,也可以使用?把變量標(biāo)記為可選。例如模板

“{controller}/{action}/{id?}”

其中id為可選變量,這樣配置后

Home/Index和Home/Index/12都會成功匹配。

6.使用*提取Path中剩余的所有部分

如果一個模板需要匹配包含任意多個部分(Segments)的Path,可以使用*符號指定變量,使用*制定過的變量會把Path中匹配完成后剩余部分全部提取出來,例如模板:

”{controller}/{action}/{id?}/{*others}”

Home/Index/12/a/b/c/d,會路由到HomeController的Index方法,id為12,others為a/b/c/d

實(shí)際上僅從模板匹配的角度來說,上面的模板可以匹配所有的Path。唯一的問題就是匹配后對應(yīng)的Controller和Action可能不存在。

7.多條路由規(guī)則的選擇

實(shí)際應(yīng)用中很可能會配置多條路由規(guī)則,當(dāng)接收到Path時很可能不止一條規(guī)則能夠匹配。

最簡單的,我們配置以下兩條模板:

“{controller }/{action =About}”

“{controller }/{action =Index }”

當(dāng)Path為Home時兩條路由都能匹配,那要怎么選擇呢?其實(shí)很簡單粗暴,就是看哪條路由在前面。也就是說Path一旦成功匹配到模板后就會立即實(shí)施路由并忽略后面的模板。對于上面的配置來說Home會被路由到HomeController的About方法。所以在配置路由時一定要注意順序。

0x02 使用Attribute配置路由

除了在Startup.cs中配置全局路由規(guī)則外,也可以針對單個Controller和其中的Action配置路由。方法就是在Controller類和Action方法上使用Route特性。例如在TestController的Show方法上使用Route特性:

[Route("TestShow")] public IActionResult Show()
{ return View();
}

當(dāng)Path為TestShow時,即可路由到TestController的Show方法。

上面我們在介紹默認(rèn)值時提到過,通過全局模板配置:

復(fù)制代碼
routes.MapRoute(
    name: "test",
    template: "TestShow",
    defaults: new {
        controller = ”Test”,
        action = ”Show”, 
    });
復(fù)制代碼

也可以達(dá)到同樣的目的。不過區(qū)別在于,使用后一種方法時,如果還有”{controller}/{action}”這樣的模板,除了TestShow外,當(dāng)Path為Test/Show可以匹配這個模板并路由到TestController的Show方法。而通過在Show方法上配置Route特性后,只有TestShow才可以路由,即使同時存在”{controller}/{action}”這樣的模板,Test/Show也無法路由。

第一次接觸用Route特性配置路由時,我很疑惑路由組件是如何把Path路由到對應(yīng)的Controller和Action的,后來下了個斷點(diǎn)看了下RouteData對象,發(fā)現(xiàn)對于配置了路由的Action方法,其controller為方法所在的Controller的名稱,action為方法的名稱,而且在Route特性配置的路由模板中不能夠使用{controller}變量和{action}變量。這樣就保證了匹配模板的Path總能路由到這個Action。

對于在Controller類上配置的Route特性最終會分別配置到Controller中的每個Action上。例如我們在TestController上配置Route(“TestShow”),實(shí)際上就是給每個方法配置了Route(“TestShow"),所以當(dāng)Path為TestShow時會報錯,提示有兩個action滿足匹配。那么應(yīng)當(dāng)如何給Controller通過Route配置路由呢,可以使用[controller]和[action]。

Route特性中的[controller]和[action]

對于[controller]和[action]我也不知道該怎么叫,不能叫變量,功能上類似占位符。當(dāng)我們在Controller類用Route特性配置路由時,如果使用了[controller]和[action],這樣當(dāng)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創(chuàng)建一個ActionDescriptor對象存儲這個Action的路由信息。對于配置了Route特性的Action(再重復(fù)一下,給Controller類配置Route特性相當(dāng)于給Controller中的每個Action配置Route特性),其ActionDescriptor中會有一個AttributeRouteInfo對象,對于未配置Route特性的Action,該對象為空。AttributeRouteInfo中包含了路由模板信息。

 

所以對于上面TestController的Route特性的配置,配置為Route(“durow/Test/[action]”)也能達(dá)到同樣的效果。不過使用Route(“durow/[controller]/[action]”)語義更強(qiáng)更通用。

在Route特性中使用變量

在Route特性中配置模板也是可以使用變量的,同樣可以使用?標(biāo)記變量可選。例如可以給TestController配置Route(“durow/[controller]/[action]/{id?}”)。但需要注意的是Route特性的模板中變量不能使用默認(rèn)值(包括[controller]和[action]),也不能使用*提取Path所有剩余部分。

0x03 寫在最后

啰啰嗦嗦居然寫了這么多,其實(shí)實(shí)際使用中很可能用不到多么復(fù)雜的路由,一般一條通用規(guī)則,一條Area相關(guān)的規(guī)則就可以了。不過詳細(xì)了解了路由規(guī)則,當(dāng)以后遇到有些奇葩的特殊需求時能夠有更加開闊的思路。后面講討論一下路由模板中的約束和自定義約束。再后面討論一下使用Areas。

0x00 路由在MVC中起到的作用

前段時間一直忙于別的事情,終于搞定了繼續(xù)學(xué)習(xí).NetCore。這次學(xué)習(xí)的主題是MVC中的路由。路由是所有MVC框架都會實(shí)現(xiàn)的一個組件,核心功能就是根據(jù)接收到的Http請求中的Path(對于http://localhost/Home/Index/12?test=555 來說,http是協(xié)議,localhost是域,Home/Index/12是Path,test=555是參數(shù))部分,依次和路由規(guī)則集合中的規(guī)則進(jìn)行匹配,匹配成功后由對應(yīng)的Controller中的對應(yīng)Action進(jìn)行Http請求的處理。匹配不到則返回404錯誤。

 

大多數(shù)MVC框架路由規(guī)則的配置都大同小異,一般都是通過模板的方式來配置路由規(guī)則。有的還支持在Controller和Action上通過Attribute(Java中叫注解)進(jìn)行更細(xì)粒度的配置。

.NetCore MVC支持通過全局的路由模板配置路由規(guī)則,也支持在Controller和Action上通過Attribute進(jìn)行細(xì)粒度的路由配置。下面先說一下在Startup.cs中配置全局路由規(guī)則。

0x01 在Startup.cs中配置路由

所謂的路由的模板就是一串字符串,當(dāng)接收到Http請求后取出其中的Path部分,和模板進(jìn)行對照,如果匹配模板則路由到對應(yīng)的Controller和Action進(jìn)行處理。我們可以在Startup.cs文件中的Configure方法中,添加MVC功能時進(jìn)行路由配置,例如:

app.UseMvc(routes => {
    routes.MapRoute(
        name: "default",
        template: "{controller}/{action}/{id}");
});

其中name為路由規(guī)則的名稱,template為路由模板。這也引出了我們第一個概念,路由模板中的變量。

1.路由模板中的變量

在模板"{controller }/{action }/{id}"中,用花括號括起來的是路由模板中的變量。例如其中變量的作用并不是必須在Path中匹配某個固定的字符串,而是起到一個占位的作用,例如上面的模板就可以匹配由“/”隔開的共三部分的Path,例如a/b/c可以匹配成功。而各個變量的值從Path中對應(yīng)部分提取出來。例如

Home/Index/12可以匹配,其中controller為Home,action為Index,id為12

Home/Index則匹配失敗,因?yàn)橹挥?部分

Home/Index/12/34同樣匹配失敗,因?yàn)槌^了3部分。

模板匹配成功后,會根據(jù)controller和action提取出的值路由:

Home/Index/12會路由到HomeController的Index方法,變量id為12

Test/Show/ab會路由到TestController的Show方法,變量id為ab

2.變量值得獲取

在Index或Show方法中,我們可以有兩種方法提取變量:

一種是在方法的參數(shù)列表中加入和變量相同名稱的參數(shù),MVC會自動從變量列表中尋找并轉(zhuǎn)換為對應(yīng)類型:

public IActionResult Index(string id, string controller, string action)
{
    ViewData["Message"] = "id is " + id + ",  controller is " + controller + ",  action is " + action; return View();
}

另一種就是從RouteData中取出:

復(fù)制代碼
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();
}
復(fù)制代碼

路由模板中的變量名稱是可以自己定義的,但controller和action(包括后面講的area)都是比較特殊的變量。其中controller提取出的值作為Controller的名稱,action提取出的值作為Controller中方法的名稱。為了讓每條路由規(guī)則都能夠路由到Controller和Action,在路由模板中都應(yīng)該出現(xiàn)controller和action變量,但我們也可以給controller和action變量指定默認(rèn)值,這樣在Path中省略了這部分時會用默認(rèn)值代替。

3.變量的默認(rèn)值

由兩種方法可以配置變量的默認(rèn)值:

app.UseMvc(routes => {
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id=0}");
});

或者

復(fù)制代碼
routes.MapRoute(
    name: "default",
    template: "{controller}/{action}/{id}",
    defaults: new {
        controller = ”Home”,
        action = ”Index”,
        id = 0,
    });
復(fù)制代碼

這樣配置后Path中帶有默認(rèn)值的部分可以省略,省略的規(guī)則和C#中帶默認(rèn)值的參數(shù)一樣,例如:

空Path會被路由到HomeController,Index方法

Test會被路由到TestController,Index方法

Test/Show依然會被路由到TestController,Show方法

一般我會用第一種方法配置默認(rèn)值,更加直觀和方便。但有時候有些需求是第一種方法難以做到的。例如我想給TestController的Show方法配置路由為TestShow,使用第一種方法可以這樣配置:”TestShow/{controller=Test}/{action=Show}”,這樣配置當(dāng)Path為TestShow時的確可以路由到TestController的Show方法,但當(dāng)Path為TestShow/Home/Index時會路由到HomeController的Index方法。

使用第二種方法配置:

復(fù)制代碼
routes.MapRoute(
    name: "test",
    template: "TestShow",
    defaults: new {
        controller = ”Test”,
        action = ”Show”, 
    });
復(fù)制代碼

當(dāng)Path為TestShow時可以路由到TestController的Show方法,但Path為Test/Home/Index則無法匹配模板。關(guān)于細(xì)粒度的路由配置更好的方法是給Test方法使用Route特性(Attribute)進(jìn)行配置,后面會說到。

4.路由規(guī)則中的靜態(tài)字符

除了使用變量來配置路由模板,還可以使用靜態(tài)字符串。靜態(tài)字符串可以單獨(dú)使用,也可以與變量混合使用。

例如模板為:

”Durow/{controller}/{action}”

Durow/Home/About會路由到HomeController,About方法

Durow/Test/Show會路由到TestController,Show方法

也可以把靜態(tài)字符和變量混合起來,例如配置模板為:

”My{controller}/My{action}”

MyHome/MyAbout會被路由到HomeController,About方法

MyTest/MyShow會被路由到TestController,Show方法

5.使用?標(biāo)記變量可選

除了通過給變量提供默認(rèn)值使其可選外,也可以使用?把變量標(biāo)記為可選。例如模板

“{controller}/{action}/{id?}”

其中id為可選變量,這樣配置后

Home/Index和Home/Index/12都會成功匹配。

6.使用*提取Path中剩余的所有部分

如果一個模板需要匹配包含任意多個部分(Segments)的Path,可以使用*符號指定變量,使用*制定過的變量會把Path中匹配完成后剩余部分全部提取出來,例如模板:

”{controller}/{action}/{id?}/{*others}”

Home/Index/12/a/b/c/d,會路由到HomeController的Index方法,id為12,others為a/b/c/d

實(shí)際上僅從模板匹配的角度來說,上面的模板可以匹配所有的Path。唯一的問題就是匹配后對應(yīng)的Controller和Action可能不存在。

7.多條路由規(guī)則的選擇

實(shí)際應(yīng)用中很可能會配置多條路由規(guī)則,當(dāng)接收到Path時很可能不止一條規(guī)則能夠匹配。

最簡單的,我們配置以下兩條模板:

“{controller }/{action =About}”

“{controller }/{action =Index }”

當(dāng)Path為Home時兩條路由都能匹配,那要怎么選擇呢?其實(shí)很簡單粗暴,就是看哪條路由在前面。也就是說Path一旦成功匹配到模板后就會立即實(shí)施路由并忽略后面的模板。對于上面的配置來說Home會被路由到HomeController的About方法。所以在配置路由時一定要注意順序。

0x02 使用Attribute配置路由

除了在Startup.cs中配置全局路由規(guī)則外,也可以針對單個Controller和其中的Action配置路由。方法就是在Controller類和Action方法上使用Route特性。例如在TestController的Show方法上使用Route特性:

[Route("TestShow")] public IActionResult Show()
{ return View();
}

當(dāng)Path為TestShow時,即可路由到TestController的Show方法。

上面我們在介紹默認(rèn)值時提到過,通過全局模板配置:

復(fù)制代碼
routes.MapRoute(
    name: "test",
    template: "TestShow",
    defaults: new {
        controller = ”Test”,
        action = ”Show”, 
    });
復(fù)制代碼

也可以達(dá)到同樣的目的。不過區(qū)別在于,使用后一種方法時,如果還有”{controller}/{action}”這樣的模板,除了TestShow外,當(dāng)Path為Test/Show可以匹配這個模板并路由到TestController的Show方法。而通過在Show方法上配置Route特性后,只有TestShow才可以路由,即使同時存在”{controller}/{action}”這樣的模板,Test/Show也無法路由。

第一次接觸用Route特性配置路由時,我很疑惑路由組件是如何把Path路由到對應(yīng)的Controller和Action的,后來下了個斷點(diǎn)看了下RouteData對象,發(fā)現(xiàn)對于配置了路由的Action方法,其controller為方法所在的Controller的名稱,action為方法的名稱,而且在Route特性配置的路由模板中不能夠使用{controller}變量和{action}變量。這樣就保證了匹配模板的Path總能路由到這個Action。

對于在Controller類上配置的Route特性最終會分別配置到Controller中的每個Action上。例如我們在TestController上配置Route(“TestShow”),實(shí)際上就是給每個方法配置了Route(“TestShow"),所以當(dāng)Path為TestShow時會報錯,提示有兩個action滿足匹配。那么應(yīng)當(dāng)如何給Controller通過Route配置路由呢,可以使用[controller]和[action]。

Route特性中的[controller]和[action]

對于[controller]和[action]我也不知道該怎么叫,不能叫變量,功能上類似占位符。當(dāng)我們在Controller類用Route特性配置路由時,如果使用了[controller]和[action],這樣當(dāng)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創(chuàng)建一個ActionDescriptor對象存儲這個Action的路由信息。對于配置了Route特性的Action(再重復(fù)一下,給Controller類配置Route特性相當(dāng)于給Controller中的每個Action配置Route特性),其ActionDescriptor中會有一個AttributeRouteInfo對象,對于未配置Route特性的Action,該對象為空。AttributeRouteInfo中包含了路由模板信息。

 

所以對于上面TestController的Route特性的配置,配置為Route(“durow/Test/[action]”)也能達(dá)到同樣的效果。不過使用Route(“durow/[controller]/[action]”)語義更強(qiáng)更通用。

在Route特性中使用變量

在Route特性中配置模板也是可以使用變量的,同樣可以使用?標(biāo)記變量可選。例如可以給TestController配置Route(“durow/[controller]/[action]/{id?}”)。但需要注意的是Route特性的模板中變量不能使用默認(rèn)值(包括[controller]和[action]),也不能使用*提取Path所有剩余部分。

0x03 寫在最后

啰啰嗦嗦居然寫了這么多,其實(shí)實(shí)際使用中很可能用不到多么復(fù)雜的路由,一般一條通用規(guī)則,一條Area相關(guān)的規(guī)則就可以了。不過詳細(xì)了解了路由規(guī)則,當(dāng)以后遇到有些奇葩的特殊需求時能夠有更加開闊的思路。后面講討論一下路由模板中的約束和自定義約束。再后面討論一下使用Areas。