asp.net mvc源码分析 - 路由注册
前言:
mvc在beta版时就开始接触了,通过博客园里很多人的分享很学到很多,在这里非常感谢他们,mvc很灵活扩展点很多。但如果没有深入了解其源码实现过程,只通一些扩展点文章了解如何扩展,会存在盲区,就是不知道为什么可以这样做。想要加深了解,读熟源码是非常重要的,只有通过其源码了解来龙去脉,才能方便的用自己的方式去扩展,以下是我以前的一次读mvc源码过程记录,很乱,这回算是整理并回顾下。
此文适合己了解asp.net mvc基本流程,想加深认识asp.net mvc的同志,是基于mvc 2.0 的,比较早了,但我觉得很多东东在现在还是差不多的,可供学习参考。如果有讲的不对的地欢迎给我指正。
初用mvc的朋友是否对添加路由规则有点迷茫,他具体是怎么来映射到控制器的。又是怎么来生成url的,要怎么合理的添加路由规则。
mvc默认给了我们很多约定,比如:你的视图文件必须放在 Views/xxx/ 文件夹里面,建一个 Area ,默认给建一个 Areas/xxx/..目录。
然后 AreaRegistration.cs 成为该area的路由注册,这里注册路由是怎么被 Global.asax 里的 AreaRegistration.RegisterAllAreas() 执行的。
是否可以把所有area路由集中放到一个地方按你定制的代码注册。是否可以把 Controller 放到别的地方去,是否想把所有 area 里面的 view 拿出来放一个专门存放 view 的项目中去?...此处省略109.5个字。
在框架各种约定下,有时会觉得不爽,不能自己随意组识自己的文件,但如果你细读源码,就发会现,其实所有的一切,都可以自己定制的。
必备 Reflector 等源码查看工具。
Routing - 路由注册(严格说Routing不只属于mvc,但我这里当作是mvc源码一部分来讲了)
mvc所有的请求都是通过路由规则去映射的,所以mvc的头等大事就是路由规则的注册,也是asp.net mvc第一件要做的事,因为规则确定了,才能知道当前请求应该映射到哪个Controller的哪个Action。
规则的注册是在 Global.asax 的 Application_Start 事件里注册,以下是默认的路由注册代码:
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
}
默认在Application_Start里调用了两个方法。AreaRegistration.RegisterAllAreas() 和 RegisterRoutes(RouteTable.Routes)
前者是注册所有的Area路由规则,area规则通常在添加area后自动产生,如Areas/xxx/xxxAreaRegistration.cs,后者是调用了当前的静态方法,注册了当前非area的路由,可以通过源码看下RegisterRoutes(RouteTable.Routes),了解mvc路由的注册。
这个方法传入了 RouteTable.Routes 这个属性,我们可以通过 Reflector 去查看 RouteTable 类。在 System.Web.Routing.dll 里面,
{
// Fields
private static RouteCollection _instance;
// Methods
static RouteTable();
public RouteTable();
// Properties
public static RouteCollection Routes { get; }
}
静态构造及Routes属性。
{
_instance = new RouteCollection();
}
public static RouteCollection Routes
{
get
{
return _instance;
}
}
通过上面的源码可以看到, RouteTable ,只有一个职责,就是构建一个单例静态 RouteCollection 类,这个类是用来保存路由规则(Route)的集合。RouteTable.Routes 属性就是指向这个静态 RouteCollection 实例,上面就是传入了这个实例,并调用其MapRoute方法添加路由规则。
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
我们再查看 RouteCollection.MapRoute(... 添加规则。当看到这个类的源码时,并没有发现有 MapRoute 这些注册方法。
通过vs里跟综可以发现他是在 System.Web.Mvc.RouteCollectionExtensions 这个类的扩展方法里的。可能是 Routing 现在作为一个单独的组件,考滤到可以在不同的需求里扩展不同的注册规则方式。
{
// Methods
private static RouteCollection FilterRouteCollectionByArea(RouteCollection routes, string areaName, out bool usingAreas);
public static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, RouteValueDictionary values);
public static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary
values);
internal static VirtualPathData GetVirtualPathForArea(this RouteCollection routes, RequestContext requestContext, string name, RouteValueDictionary
values, out bool usingAreas);
public static void IgnoreRoute(this RouteCollection routes, string url);
public static void IgnoreRoute(this RouteCollection routes, string url, object constraints);
public static Route MapRoute(this RouteCollection routes, string name, string url);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults);
public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces);
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces);
// Nested Types
private sealed class IgnoreRouteInternal : Route
{
// Methods
public IgnoreRouteInternal(string url);
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues);
}
}
上面可以看到有IgnoreRoute 方法,默认的第一条路由就是通过这个方法注册,如: routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 这个。
这也是一个注册路由方法,这样的规则是用来干嘛?这里先不作分析,等下几篇会详细再讲。可以看到上面己经有MapRoute
这个注册方法了,都是一些重载,我们点开最后一个看具体实现。
{
if (routes == null)
{
throw new ArgumentNullException("routes");
}
if (url == null)
{
throw new ArgumentNullException("url");
}
Route route2 = new Route(url, new MvcRouteHandler()) {
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints),
DataTokens = new RouteValueDictionary()
};
Route item = route2;
if ((namespaces != null) && (namespaces.Length > 0))
{
item.DataTokens["Namespaces"] = namespaces;
}
routes.Add(name, item);
return item;
}
上面的代创建了一个 Route ,Route就是一条具体的路由,下次再单独详细Route分析这个类,因为Route是路由映射中一个很重要的成员。这里我们只要知道Route就是具体的一条路由规则,通过包装传入的参数后被添加到了RouteTable.Routes(全局路由集合),这是在整个网站的生命周期里一直随时可以访问路由集合。为后面的请求或是生成url起着重要的作用。这些后续会去分析。
我们可以在Application_BeginRequest打印所有己注册的规则url,同时以后我们可以这样去测试url的有效性及匹配情况。这些后续会提到。
{
RouteCollection routes = RouteTable.Routes;
foreach (Route route in routes)
{
Response.Write(route.Url + "<br />");
}
}
上面就是默认非area的路由注册,我们再看下area的注册又是如何进行的。
自从asp.net mvc2.0 就引入了area,可以让我们把不同的模块分格开来,我们默认添加一个 area,自动在 Areas 文件下创建。然后又为每个area添加了一个注册规则的文件,如:Areas/Admin/AdminAreaRegistration.cs
那么这里面的规则又是如何被注册到的呢,这里的又和前面的有什么区别,可以把area的路由注册放到 Application_Start 里去注册吗?
我们在Application_Start里看到先是调用了AreaRegistration.RegisterAllAreas(),这个就是先注册所有area的路由。我们去看下这个方法。
AreaRegistration 这个类是在 System.Web.Mvc.dll ,你可以通过下载mvc的源码查看,我这里是用 Reflector 查看,这个类被定义为abstract。实际我们创建的Areas里的xxxAreaRegistration.cs,就是继承这个类,并实现了RegisterArea(AreaRegistrationContext context) , AreaName { get; }
AreaRegistration 类
{
// Fields
private const string _typeCacheName = "MVC-AreaRegistrationTypeCache.xml";
// Methods
protected AreaRegistration();
internal void CreateContextAndRegister(RouteCollection routes, object state);
private static bool IsAreaRegistrationType(Type type);
public static void RegisterAllAreas();
public static void RegisterAllAreas(object state);
internal static void RegisterAllAreas(RouteCollection routes, IBuildManager buildManager, object state);
public abstract void RegisterArea(AreaRegistrationContext context);
// Properties
public abstract string AreaName { get; }
}
首先看下他的静态方法先后调用
{
RegisterAllAreas(null);
}
public static void RegisterAllAreas(object state)
{
RegisterAllAreas(RouteTable.Routes, new BuildManagerWrapper(), state);
}
internal static void RegisterAllAreas(RouteCollection routes, IBuildManager buildManager, object state)
{
foreach (Type type in TypeCacheUtil.GetFilteredTypesFromAssemblies("MVC-AreaRegistrationTypeCache.xml", new Predicate<Type>(AreaRegistration.IsAreaRegistrationType), buildManager))
{
((AreaRegistration) Activator.CreateInstance(type)).CreateContextAndRegister(routes, state);
}
}
可以看到最终是是通过TypeCacheUtil.GetFilteredTypesFromAssemblies过滤找所有MVC-AreaRegistrationTypeCache.xml这样一个常量标识的类,而上面的AreaRegistration 里恰好有这个值为 MVC-AreaRegistrationTypeCache.xml 常量,可能是做为识别标识,这部分比较复杂,而且也没太大必要去细做研究。我们只要大概猜到,这里就是取所有继承了AreaRegistration的类(就是我们创建area后自带的注册路由的类)。并创建实例,然后调用 CreateContextAndRegister(routes, state); routes是前讲到的全局路由集合,state,这里上面是传入了null。我们再去看下AreaRegistration的这个CreateContextAndRegister方法。
{
AreaRegistrationContext context = new AreaRegistrationContext(this.AreaName, routes, state);
string str = base.GetType().Namespace;
if (str != null)
{
context.Namespaces.Add(str + ".*");
}
this.RegisterArea(context);
}
首先是创建了一个 AreaRegistrationContext,这个类我们可以理解为一个打包。就是把 AreaName Routes state这些东东打包,以下构造方法。
{
this._namespaces = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(areaName))
{
throw Error.ParameterCannotBeNullOrEmpty("areaName");
}
if (routes == null)
{
throw new ArgumentNullException("routes");
}
this.AreaName = areaName;
this.Routes = routes;
this.State = state;
}
然后如果非null就把空间命名也加入到AreaRegistrationContext的Namespaces属性集合中。
最后把包装好的数据传入调用了 this.RegisterArea(context); 这就是我们 Areas/xxx/xxxAreaRegistration.cs 里实现了AreaRegistration的方法,完成了当前area的路由规则注册。
{
public override string AreaName
{
get
{
return "Admin";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
再看一下这里的RegisterArea里的注册是通过 AreaRegistrationContext 类的 MapRoute 方法了,而不是上篇非area注时册的RouteCollection扩展MapRoute方法进行注册了,我们去看下 AreaRegistrationContext 这个类的MapRoute方法是怎样的。
{
if ((namespaces == null) && (this.Namespaces != null))
{
namespaces = this.Namespaces.ToArray<string>();
}
Route route = this.Routes.MapRoute(name, url, defaults, constraints, namespaces);
route.DataTokens["area"] = this.AreaName;
bool flag = (namespaces == null) || (namespaces.Length == 0);
route.DataTokens["UseNamespaceFallback"] = flag;
return route;
}
看标注的那行。this.Routes ,就是前打包 AreaRegistrationContext 的值,实际还是 RouteTable.Routes,也就是最后还是和非area一样。只是后面多了些操作。就是往 route.DataTokens 属性里,加了两项值。这也是与非area规注注册的一点区别。
bool flag = (namespaces == null) || (namespaces.Length == 0);
route.DataTokens["UseNamespaceFallback"] = flag;
这里我们基本可以确定,只要 Route 的 DataTokens 属性里,有 area 这项值,就是一个area规则。
UseNamespaceFallback 这项值我以前的记录里没有,我记得以前整个mvc流程完了,好像也没太看到这个属性用在哪,这里先不管,不加也一样。分析到再说吧。我们现在可以试一下,把area 里的 AdminAreaRegistration.cs 删了。把area的路由规则放在Application_Start()里去注册,如下:
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//这里是area
Route route = routes.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new string[] { "MVCTest.Areas.Admin.Controllers" }
);
route.DataTokens["area"] = "Admin";
//这里是area
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new string[] { "MVCTest.Controllers" }
);
}
protected void Application_Start()
{
//AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
然后访问 /admin/home 我们可以发现可以正常请求area的。
这样如果喜欢把规则放到一起注册的朋友可以这样了,不必再在每个 xxxAreaRegistration.cs 文件里注册了。
或者用你自己的方式,反正简单的说,注册路由规则,就是往 RouteCollection 集合里添加 Route,而area的注册,就是 Route的DataTokens["area"] = "Admin"; 指定其area名称。而当 DataTokens 含有 area 值,mvc后面生成url和指定view都会用到这个值,后续我们会看到。
结尾语:
本文会跟着mvc的整个执行流程依次把多数源码分析下去。其中结合了自己的一些见解,如果有什么不正确的地方还请指出。
转载请注明出处,谢谢。