mvc源码解读(2)-mvc路由注册
mvc虽然开源了,但是mvc核心的路由注册机制微软并没有公开,因此在开源的mvc源码中我们并不能完全的分析到mvc的路由注册原理,我们必须借助反编译工具来查看路由的注册。我们在新建一个mvc项目的时候,在全局文件Global.asax进行初始化的时候都会有一个注册路由的RegisterRoutes方法,该方法具体如下:
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.IgnoreRoute("{folder}/{*pathInfo}", new { folder = "Service" }); routes.MapPageRoute("CommonReportRoute", // Route name " Reports/{reportmodel}/{reportname}",// URL "~/Reports/{reportmodel}.aspx" // File ); routes.MapRoute("Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ).DataTokens["UseNamespaceFallback"] = false; } |
蓝色代码表示映射页面路由信息,为什么会有映射页面类的文件路由信息呢?因为在mvc3中我们我们可以创建两种视图引擎,一种是.aspx视图引擎,另外一种就是Razor视图引擎。我们来看看路由集合RouteCollection中的MapRoute方法,该方法有很多个重载的方法,但是所有的重载方法都统一的调用了一个核心的MapRoute方法,该方法具体实现如下:
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
{ if ((namespaces != null) && (namespaces.Length > 0))
route.DataTokens["Namespaces"] = namespaces; return route; |
该方法里面涉及到一个很重要的类Route,Asp.net mvc中正是通过该类实现了基于URL模板的路由机制。我们应该还记得在上一篇文章中的介绍的PostResolveRequestCache事件,该事件里面的第一句话就是:
RouteData routeData = this.RouteCollection.GetRouteData(context); |
来获取路由信息,GetRouteData方法属于路由集合类RouteCollection,具体实现如下:
public RouteData GetRouteData(HttpContextBase httpContext)
{
if (base.Count != 0)
{
bool flag = false;
bool flag2 = false;
if (!this.RouteExistingFiles)
{
flag = this.IsRouteToExistingFile(httpContext);
flag2 = true;
if (flag)
{
return null;
}
}
using (this.GetReadLock())
{
foreach (RouteBase base2 in this)
{
RouteData routeData = base2.GetRouteData(httpContext);
if (routeData != null)
{
if (!base2.RouteExistingFiles)
{
if (!flag2)
{
flag = this.IsRouteToExistingFile(httpContext);
flag2 = true;
}
if (flag)
{
return null;
}
}
return routeData;
}
}
}
}
return null;
}
|
该方法返回的routeData是由抽象类RouteBase的GetRouteData方法获取的,而Route实现了该抽象类的GetRouteData方法,因此我们可以知道base2指的就是Route对象。该方法具体实现如下:
public override RouteData GetRouteData(HttpContextBase httpContext) { string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo; RouteValueDictionary values = this._parsedRoute.Match(virtualPath, this.Defaults); if (values == null) { return null; } RouteData data = new RouteData(this, this.RouteHandler); if (!this.ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest)) { return null; } foreach (KeyValuePair<string, object> pair in values) { data.Values.Add(pair.Key, pair.Value); } if (this.DataTokens != null) { foreach (KeyValuePair<string, object> pair2 in this.DataTokens) { data.DataTokens[pair2.Key] = pair2.Value; } } return data; } |
在这个方法里面直接返回了一个RouteData对象,但是该对象已经携带了特殊的信息。我们主要来看一下这一句代码:
string virtualPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo; |
httpContext.Request属性返回的是HttpRequest对象,该对象的AppRelativeCurrentExecutionFilePath属性返回的是方法 MakeVirtualPathAppRelative返回的值,该方法具体实现如下:
internal static string MakeVirtualPathAppRelative(string virtualPath, string applicationPath, bool nullIfNotInApp) { int length = applicationPath.Length; int num2 = virtualPath.Length; if ((num2 == (length - 1)) && StringUtil.StringStartsWithIgnoreCase(applicationPath, virtualPath)) { return "~/"; } if (!VirtualPathStartsWithVirtualPath(virtualPath, applicationPath)) { if (nullIfNotInApp) { return null; } return virtualPath; } { return "~/"; } if (length == 1) { return ('~' + virtualPath); } return ('~' + virtualPath.Substring(length - 1)); } |
由这个方法我们可以知道httpContext.Request.AppRelativeCurrentExecutionFilePath通常返回的就是一个以“~/”开头的url,httpContext.Request.PathInfo在一般情况下返回的也是空,这里面的具体实现我们在此略过,里面的逻辑较为复杂。因此我们可以根据上面的全局文件注册的方法可以知道:假如我们要请求的URl为http://localhost:7989/Home/index的话,则httpContext.Request.AppRelativeCurrentExecutionFilePath返回的值就是:~/Home/index,因此virtualPath的值就是:Home/index。
我们再来看看这一句代码: RouteValueDictionary values = this._parsedRoute.Match(virtualPath, this.Defaults);Match方法的具体实现如下:
public RouteValueDictionary Match(string virtualPath, RouteValueDictionary defaultValues) |
首先看红色代码:IList<string> source = RouteParser.SplitUrlToPathSegmentStrings(virtualPath);SplitUrlToPathSegmentStrings方法在密封类ParsedRoute的实现如下:
internal static IList<string> SplitUrlToPathSegmentStrings(string url) { List<string> list = new List<string>(); if (!string.IsNullOrEmpty(url)) { int index; for (int i = 0; i < url.Length; i = index + 1) { index = url.IndexOf('/', i); if (index == -1) { string str = url.Substring(i); if (str.Length > 0) { list.Add(str); } return list; } string item = url.Substring(i, index - i); if (item.Length > 0) { list.Add(item); } list.Add("/"); } } return list; } |
依然以我们刚才的例子来做解释:传进来的参数url=virtualPath就是Home/Index,经过SplitUrlToPathSegmentStrings方法处理之后返回的list集合中有三个元素:list[0]="Home",list[1]="/",list[2]="Index";其实里面获取路由信息的方法是相当的繁琐和复杂的,我看了很久了源码都没有完全的读懂,后来参考了博客园另一位仁兄dz45693的关于读取路由信息详细解析,才慢慢领悟,大家可以参考一下他的这篇文章http://www.cnblogs.com/majiang/archive/2012/11/21/2780591.html讲的很详细,值得大家的学习。我们再回到GetRouteData方法中,我们获取到相匹配的RouteValueDictionary之后,遍历RouteValueDictionary对象values,将通过地址解析出来的变量存放到RouteData对象的Values属性中,DataTokens 属性检索或分配与路由关联且未用于确定该路由是否匹配 URL 模式的值,这些值会传递到路由处理程序,以便用于处理请求。
关于mvc的路由信息,这里面还牵涉到一个十分重要的类:RouteTable,顾名思义就是路由表的意思,这是由于我们的mvc web程序中可能会有不同的url模式,可能并不是默认的~/{Controller}/{Action}/{Parameters},有可能是~/Action}/{Controller}/{Parameters},因此用RouteTable来表示整个mvc应用的全局路由信息。这个RouteTable的定义:
public class RouteTable { private static RouteCollection _instance = new RouteCollection(); public static RouteCollection Routes { get { return _instance; } } } |
RouteTable是对RouteCollection的封装,顾名思义RouteCollection就是路由集合的意思,我们来看RouteCollection里面的Add方法定义:
public void Add(string name, RouteBase item) { base.Add(item); if (!string.IsNullOrEmpty(name)) { this._namedMap[name] = item; } } |
这里面_namedMap是一个字典集合,定义如下:
private Dictionary<string, RouteBase> _namedMap; |
RouteDictionary表示一个具体的路由对象的列表,其中的Key表示注册的路由对象的名称,具体的Route对象为value。例如:
RouteTable.Routes.Add("myTest", new Route { url = "{controller}/{action}/{Parameter}" }); |
理解了这一点之后,我们回过头来看看RouteCollection类里面的GetRouteData方法里面的这一句代码:
foreach (RouteBase base2 in this) |
就是在循环便遍历 Dictionary<string, RouteBase>集合,从中找到相互匹配的路由对象,并返回RouteData。最后我们再次回到RouteCollection中的MapRoute方法,将路由名称和路由对象添加到RouteCollection中,实现了将url映射到相应的Controller和Action。我们获取到RouteData之后,将其作为参数获取到RequestContext,将获取到的RequestContext作为参数来获取到MvcHnadler,并将其注册到事件管道中,这里就是我们上一篇文章介绍的内容。