我們在ASP.NET Core MVC中如果要啟用Area功能,那么會看到在Startup類的Configure方法中是這么定義Area的路由的:
app.UseMvc(routes => { routes.MapRoute( name: "subAreaRoute", template: "{area:exists}/{subarea:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "areaRoute", template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
我們可以看到其中{area:exists}和{subarea:exists}這兩個路由參數后面都有個:exists后綴,那么這是用來干什么的呢?
來舉個例子就明白了:
如果現在ASP.NET Core MVC項目中有個HomeController,並且HomeController下有名為Index的Action,如下所示:
HomeController:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; namespace WebCore.Controllers { public class HomeController : Controller { public IActionResult Index() { return View(); } } }
並且Index這個cshtml文件的View也存在:
然后項目中沒有定義任何Area文件夾
現在我們訪問如下Url:
http://localhost:49908/Home/Index
該Url可以成功顯示Index視圖,這是因為該Url成功配匹到了ASP.NET Core MVC路由"default",也就是"{controller=Home}/{action=Index}/{id?}"
但是如果我們現在將ASP.NET Core MVC的路由配置改成下面這樣,去掉"subAreaRoute"路由的{area}和{subarea}路由參數的:exists后綴:
app.UseMvc(routes => { routes.MapRoute( name: "subAreaRoute", template: "{area}/{subarea}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "areaRoute", template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
我們再訪問Url:
http://localhost:49908/Home/Index
結果如下:
很明顯路由解析失敗了,ASP.NET Core MVC拋出了異常,可這是為什么呢?我們的項目中有HomeController也有Index這個Action,也有Index.cshtml視圖,為什么Url地址http://localhost:49908/Home/Index沒有成功匹配到呢?
這是因為當我們去掉了ASP.NET Core MVC路由"subAreaRoute"的路由參數:exists后綴后,Url地址http://localhost:49908/Home/Index匹配到的是"subAreaRoute"路由”{area}/{subarea}/{controller=Home}/{action=Index}/{id?}“,並不是"default"路由"{controller=Home}/{action=Index}/{id?}",我們來看看Url地址為什么能匹配"subAreaRoute"路由”{area}/{subarea}/{controller=Home}/{action=Index}/{id?}“:
- 首先Url地址http://localhost:49908/Home/Index中的Home,匹配到"subAreaRoute"路由”{area}/{subarea}/{controller=Home}/{action=Index}/{id?}“中的路由參數{area},所以路由參數{area}的值為Home
- 然后Url地址http://localhost:49908/Home/Index中的Index,匹配到"subAreaRoute"路由”{area}/{subarea}/{controller=Home}/{action=Index}/{id?}“中的路由參數{subarea},所以路由參數{subarea}的值為Index
- 接下來Url地址http://localhost:49908/Home/Index沒有信息可以提供給"subAreaRoute"路由了,所以"subAreaRoute"路由”{area}/{subarea}/{controller=Home}/{action=Index}/{id?}“中的{controller}路由參數為默認值Home,{action}路由參數為默認值Index,{id}路由參數由於是可以為空的所以沒有值。這樣"subAreaRoute"路由的所有路由參數都有值了,所以Url地址http://localhost:49908/Home/Index匹配路由"subAreaRoute"成功了,它就不會再去匹配路由表中的"areaRoute"路由"{area:exists}/{controller=Home}/{action=Index}/{id?},和"default"路由"{controller=Home}/{action=Index}/{id?}"了
現在Url地址http://localhost:49908/Home/Index匹配路由"subAreaRoute"成功了,接下來就要根據路由參數的值去尋找相應的項目文件了:
- 由於現在"subAreaRoute"路由的{area}路由參數值為Home,而{area}路由參數為ASP.NET Core MVC的系統路由參數,其代表的是項目中的Area文件夾,那么需要項目中存在一個叫Home的Area文件夾
- 其次由於"subAreaRoute"路由的{controller}路由參數也是ASP.NET Core MVC的系統路由參數,其代表MVC控制器的名稱,{controller}路由參數值為Home,所以它要求在Home文件夾下要有一個HomeController文件
- 然后由於"subAreaRoute"路由的{action}路由參數也是ASP.NET Core MVC的系統路由參數,其代表的是MVC的Action名稱,{action}路由參數值為Index,所以它要求HomeController里面因該有一個叫Index的Action方法
然而我們的項目中並沒有叫Home的Area文件夾,因此"subAreaRoute"路由的{controller}路由參數和{action}路由參數也無法找到對應的Controller文件,所以雖然Url地址http://localhost:49908/Home/Index匹配"subAreaRoute"路由成功了,但是由於找不到Area文件夾所以最后ASP.NET Core MVC報錯了拋出了異常,其錯誤描述頁也顯示ASP.NET Core MVC試圖尋找Area文件夾的地址失敗了:
所以我們可以看到當去掉"subAreaRoute"路由”{area:exists}/{subarea:exists}/{controller=Home}/{action=Index}/{id?}“的:exists后綴后,相當於ASP.NET Core MVC不會關心匹配到的{area}路由參數是否在項目中真的有對應的Area文件夾,只要Url地址http://localhost:49908/Home/Index能提供"subAreaRoute"路由的所有路由參數值,那么路由匹配就算成功。而:exists后綴可以保證不僅路由參數能從Url地址匹配到值,還要確保路由參數值在項目中能找到真正的文件夾或文件,整個路由才算匹配成功。如果匹配失敗,Url地址會去匹配ASP.NET Core MVC中的其它路由。
我們可以再將"subAreaRoute"路由改為”{area:exists}/{subarea}/{controller=Home}/{action=Index}/{id?}“只給{area}參數加上:exists后綴:
app.UseMvc(routes => { routes.MapRoute( name: "subAreaRoute", template: "{area:exists}/{subarea}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "areaRoute", template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
然后在瀏覽器還是輸入Url地址:
http://localhost:49908/Home/Index
結果如下:
頁面成功顯示,因為現在Url地址http://localhost:49908/Home/Index匹配"subAreaRoute"路由”{area:exists}/{subarea}/{controller=Home}/{action=Index}/{id?}“失敗了,Url最終來會匹配到的路由表中的"default"路由"{controller=Home}/{action=Index}/{id?}",而"default"路由"{controller=Home}/{action=Index}/{id?}"是可以找到對應的HomeController及Index這個Action和Index.cshtml視圖的,所以頁面顯示成功。那么我們再來分析下為什么現在Url地址http://localhost:49908/Home/Index匹配"subAreaRoute"路由”{area:exists}/{subarea}/{controller=Home}/{action=Index}/{id?}“失敗了,最終會匹配"default"路由"{controller=Home}/{action=Index}/{id?}"成功:
- 首先Url地址http://localhost:49908/Home/Index中的Home,會匹配"subAreaRoute"路由”{area:exists}/{subarea}/{controller=Home}/{action=Index}/{id?}“中的{area}路由參數,其路由參數值為Home,但是現在{area}路由參數帶:exists后綴,又加上{area}路由參數是ASP.NET Core MVC的系統路由參數,代表的是項目中的Area文件夾,所以現在ASP.NET Core MVC立即會去檢查項目中是否存在一個叫Home的Area文件夾,結果在項目中找不到叫Home的Area文件夾,這樣ASP.NET Core MVC就會認為Url地址http://localhost:49908/Home/Index匹配"subAreaRoute"路由”{area:exists}/{subarea}/{controller=Home}/{action=Index}/{id?}“的{area}參數失敗,所以Url地址無法匹配路由"subAreaRoute"。
- 接下來由於ASP.NET Core MVC中還有兩個路由"areaRoute"和"default",所以Url地址http://localhost:49908/Home/Index會去嘗試匹配"areaRoute"路由"{area:exists}/{controller=Home}/{action=Index}/{id?}",因為和上面一點同樣的原因,"areaRoute"路由"{area:exists}/{controller=Home}/{action=Index}/{id?}"的{area}路由參數會匹配失敗,導致Url也無法匹配"areaRoute"路由
- 最后ASP.NET Core MVC中還剩路由"default",Url地址http://localhost:49908/Home/Index會和"default"路由"{controller=Home}/{action=Index}/{id?}"匹配成功,原因就不再介紹了,最后瀏覽器通過"default"路由"{controller=Home}/{action=Index}/{id?}"成功返回了正確的頁面
所以:exists后綴在ASP.NET Core MVC路由中有驗證文件夾和文件是否存在的功能,只有當路由參數值對應的文件夾和文件在項目中確實存在時,路由匹配才算成功,否者路由匹配失敗,會繼續用Url去匹配 ASP.NET Core MVC路由表中的后續路由。
Url地址太長也會匹配失敗
當然如果Url地址太長也會導致匹配失敗,假如我們在瀏覽器中輸入如下Url地址:
http://localhost:49908/Home/Index/1/2/3/4
還是去匹配如下三個路由,在ASP.NET Core中Startup類的Configure方法中:
app.UseMvc(routes => { routes.MapRoute( name: "subAreaRoute", template: "{area:exists}/{subarea:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "areaRoute", template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
毫無疑問"subAreaRoute"路由和"areaRoute"路由都會匹配失敗,因為我們的項目中壓根就沒有定義Area文件夾。那么這個Url地址匹配"default"路由的時候會發生什么情況呢?
我們來分析下:
- 首先Url地址http://localhost:49908/Home/Index/1/2/3/4中的Home,會為"default"路由"{controller=Home}/{action=Index}/{id?}"中的路由參數{controller}提供參數值Home
- 接下來Url地址http://localhost:49908/Home/Index/1/2/3/4中的Index,會為"default"路由"{controller=Home}/{action=Index}/{id?}"中的路由參數{action}提供參數值Index
- 然后Url地址http://localhost:49908/Home/Index/1/2/3/4中的1,會為"default"路由"{controller=Home}/{action=Index}/{id?}"中的路由參數{id}提供參數值1
- 那么問題來了,剩下在Url地址http://localhost:49908/Home/Index/1/2/3/4中沒有標注成藍色部分的/2/3/4該去匹配"default"路由"{controller=Home}/{action=Index}/{id?}"的什么路由參數呢?答案是匹配不到任何"default"路由的參數,因為"default"路由的所有參數都被匹配完了,最終Url地址http://localhost:49908/Home/Index/1/2/3/4中剩下的/2/3/4會導致Url地址http://localhost:49908/Home/Index/1/2/3/4匹配"default"路由也失敗,因為Url太長了有多余的部分無法被"default"路由匹配到。
所以最終Url地址http://localhost:49908/Home/Index/1/2/3/4和"subAreaRoute"路由、"areaRoute"路由及"default"路由都匹配失敗了,該Url地址和ASP.NET Core MVC中定義的所有路由都匹配失敗了,瀏覽器頁面最終返回404錯誤:
但是我們如果用Url地址:
http://localhost:49908/Home/Index/1
是可以匹配到"default"路由"{controller=Home}/{action=Index}/{id?}"的,因為該Url恰好提供了"default"路由的所有路由參數值:
在ASP.NET Core MVC中Url地址提供的路由參數太少會導致路由匹配失敗, Url地址提供的路由參數太多也會導致路由匹配失敗,只有當Url地址提供了合適的路由參數時,才會令路由匹配成功,否者瀏覽器會返回404錯誤匹配不到任何路由。匹配到一個路由后還要去看能否根據路由參數找到對應的項目文件和文件夾,如果無法找到又會像前面我們討論:exists后綴時那樣,頁面會拋出異常。所以一個Url地址在ASP.NET Core MVC中最后能正確地返回一個頁面,提供的信息必須要恰到好處才行,否則ASP.NET Core MVC最終都無法返回正確的結果。
{controller}和{action}路由參數默認是帶:exists后綴的
ASP.NET Core MVC的{controller}和{action}這兩個系統路由參數,分別代表的是Controller的名稱和Controller下Action方法的名稱,這兩個路由參數我發現比較特殊,因為它們默認就是帶:exists后綴的,拿本例中的"default"路由來舉例,也就是說"default"路由定義為"{controller=Home}/{action=Index}/{id?}"和"{controller:exists=Home}/{action:exists=Index}/{id?}"效果是一樣的,其效果是都帶:exists后綴,ASP.NET Core MVC都會在匹配路由時,去檢查{controller}路由參數的值是否在項目中存在Controller類及文件,也會去檢查{action}路由參數的值能否找到對應的Action方法。
還是用我們在本例Startup類的Configure方法中配置的路由表:
app.UseMvc(routes => { routes.MapRoute( name: "subAreaRoute", template: "{area:exists}/{subarea:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "areaRoute", template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
我們來匹配如下Url地址:
http://localhost:49908/User/Login
那么Url地址http://localhost:49908/User/Login會匹配"subAreaRoute"路由"{area:exists}/{subarea:exists}/{controller=Home}/{action=Index}/{id?}"和"areaRoute"路由"{area:exists}/{controller=Home}/{action=Index}/{id?}"失敗,原因如我們前面匹配Url地址http://localhost:49908/Home/Index時所述。現在Url地址http://localhost:49908/User/Login會嘗試匹配"default"路由"{controller=Home}/{action=Index}/{id?}",那么如果{controller}路由參數和{action}路由參數假如不像我們這里所說是默認帶:exists后綴的,其匹配過程應該如下:
- 首先Url地址http://localhost:49908/User/Login中的User,會匹配到匹配"default"路由"{controller=Home}/{action=Index}/{id?}"的{controller}路由參數,路由參數值為User
- 接着Url地址http://localhost:49908/User/Login中的Login,會匹配到匹配"default"路由"{controller=Home}/{action=Index}/{id?}"的{action}路由參數,路由參數值為action
- 現在Url地址http://localhost:49908/User/Login沒有內容可以提供給"default"路由"{controller=Home}/{action=Index}/{id?}"作為路由參數值了,再加上"default"路由的{id}路由參數可空可以忽略,所以"default"路由"{controller=Home}/{action=Index}/{id?}"的所有路由參數都已經匹配到參數值了,那么Url地址http://localhost:49908/User/Login匹配"default"路由"{controller=Home}/{action=Index}/{id?}"成功,接下來該去尋找項目文件了
- 由於"default"路由"{controller=Home}/{action=Index}/{id?}"的{controller}路由參數為ASP.NET Core MVC的系統路由參數,代表Controller的類名及文件,其值為User,所以現在ASP.NET Core MVC會去檢查項目中是否有UserController類及文件,結果找不到,所以瀏覽器按道理應該收到一個異常錯誤信息,顯示尋找項目文件失敗
那么結果真如我們上面四點所說嗎,我們來試下看下面截圖:
結果顯示Url地址http://localhost:49908/User/Login直接返回了404錯誤,壓根就沒和"default"路由"{controller=Home}/{action=Index}/{id?}"匹配成功,因為如果和路由匹配成功但是找不到項目文件,瀏覽器收到的應該是異常錯誤頁面而不是404頁面。所以這驗證了我們所說{controller}路由參數和{action}路由參數是默認帶:exists后綴,只有帶:exists后綴的路由參數,才會把尋找項目文件和文件夾也作為路由匹配中的一個環節,而不是等路由匹配成功后才去尋找項目文件和文件夾。因為Url地址http://localhost:49908/User/Login匹配"default"路由"{controller=Home}/{action=Index}/{id?}"時,{controller}路由參數是默認帶:exists后綴的,其路由參數值為User,所以Url地址http://localhost:49908/User/Login在匹配"default"路由時就會去檢查項目中是否存在UserController類及文件,但是找不到,所以Url地址http://localhost:49908/User/Login和"default"路由"{controller=Home}/{action=Index}/{id?}"匹配失敗,而ASP.NET Core MVC路由表中的所有路由("subAreaRoute","areaRoute","default")都和Url地址http://localhost:49908/User/Login匹配失敗了,所以最終瀏覽器出現了上面的404錯誤。