ASP.NET Core MVC的路由参数中:exists后缀有什么作用,顺便谈谈路由匹配机制
我们在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错误。