上一篇主要分析了asp.net mvc注册路由的具体细节,包括注册area路由时的区别。mvc源码分析 - 路由注册
这节主要分析一个具体的请求是如何通过我们注册的路由映射到具体的操作上。这节的知识需要了解asp.net的生命周期,如果您对这个还不了解,可以去看下asp.net 生命周期这篇文章,作为asp.net开发人员,这个必须要了解。
了解了asp.net生命周期我们知道asp.net在请求过程中会激发HttpApplication的的一系列事件,而我们可以通过实现IHttpModule去订阅这些事件并做些事,甚至是指定此次请求的HttpHandler
<httpModules>
...
<add name=
"
UrlRoutingModule
" type=
"
System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35
"
/>
</httpModules>
Mvc项目配置文件httpModules节点里加入了Routing的UrlRoutingModle,也是做为一个mvc请求的入口点,去看下具体的实现。
View Code
public
class UrlRoutingModule : IHttpModule
{
//
Fields
private
static
readonly
object _requestDataKey;
private RouteCollection _routeCollection;
//
Methods
static UrlRoutingModule();
public UrlRoutingModule();
protected
virtual
void Dispose();
protected
virtual
void Init(HttpApplication application);
private
void OnApplicationPostMapRequestHandler(
object sender, EventArgs e);
private
void OnApplicationPostResolveRequestCache(
object sender, EventArgs e);
public
virtual
void PostMapRequestHandler(HttpContextBase context);
public
virtual
void PostResolveRequestCache(HttpContextBase context);
void IHttpModule.Dispose();
void IHttpModule.Init(HttpApplication application);
//
Properties
public RouteCollection RouteCollection {
get;
set; }
//
Nested Types
private
class RequestData
{
//
Methods
public RequestData();
//
Properties
public IHttpHandler HttpHandler {
get;
set; }
public
string OriginalPath {
get;
set; }
}
}
系统会先调用所有实现了IHttpModule 的 Init 方法来注册生命周期事件的订阅。下面是UrlRoutingModule的细节。
void IHttpModule.Init(HttpApplication application)
{
this.Init(application);
}
protected
virtual
void Init(HttpApplication application)
{
application.PostResolveRequestCache +=
new EventHandler(
this.OnApplicationPostResolveRequestCache);
application.PostMapRequestHandler +=
new EventHandler(
this.OnApplicationPostMapRequestHandler);
}
UrlRoutingModule订阅了两个事件,不太熟悉的朋友可以去搜下asp.net生命周期,有很多这样的文章里,说明了所有事件的执行顺序及作用。我们打开会先执行到的事件方法。
View Code
private
void OnApplicationPostResolveRequestCache(
object sender, EventArgs e)
{
//
这里把 HttpContext 包装成 HttpContextWrapper,主要是为了利于单元测试。(HttpContextWrapper实现了HttpContextBase)
HttpContextBase context =
new HttpContextWrapper(((HttpApplication) sender).Context);
this.PostResolveRequestCache(context);
//
这里调用了下面的方法
}
public
virtual
void PostResolveRequestCache(HttpContextBase context)
{
RouteData routeData =
this
.RouteCollection.GetRouteData(context);
if (routeData !=
null)
{
IRouteHandler routeHandler = routeData.RouteHandler;
if (routeHandler ==
null)
{
throw
new InvalidOperationException(
string.Format(CultureInfo.CurrentUICulture, RoutingResources.UrlRoutingModule_NoRouteHandler,
new
object
[
0]));
}
if (!(routeHandler
is StopRoutingHandler))
{
RequestContext requestContext =
new RequestContext(context, routeData);
IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
if (httpHandler ==
null)
{
throw
new InvalidOperationException(
string.Format(CultureInfo.CurrentUICulture, RoutingResources.UrlRoutingModule_NoHttpHandler,
new
object[]
{ routeHandler.GetType() }));
}
RequestData data2 =
new RequestData {
OriginalPath = context.Request.Path,
HttpHandler = httpHandler
};
context.Items[_requestDataKey] = data2;
context.RewritePath(
"
~/UrlRouting.axd
");
}
}
}
可以看到调用了我上一篇提到的 RouteCollection类的GetRouteData(context) 方法返回一个RouteData,而这里的RouteCollection实际就是上篇讲到的RouteTable.Routes
可以点开看下 this.RouteCollection
public RouteCollection RouteCollection
{
get
{
if (
this._routeCollection ==
null)
{
this
._routeCollection = RouteTable.Routes;
}
return
this._routeCollection;
}
set
{
this._routeCollection = value;
}
}
到这里,暂时先不往下分析了,我们有必要先去了解 Routing 组件的几个主要成员。这样才能更好的了解asp.net mvc整个执行过程。
RouteTalbe
RouteCollection
RouteBase
Route
RouteData
RouteValueDictionary
RouteTalbe 职责比较简单,在上篇注册中我们分析的比较清楚了。这里就不再复述。不清楚的,可去看下。
先来回顾一下上篇分析到的注册路由的情况。
View Code
public
static Route MapRoute(
this RouteCollection routes,
string name,
string url,
object defaults,
object constraints,
string[] namespaces)
{
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;
}
在RouteCollection扩展方法MapRoute中,首先创建了一个 Route,并传入了规则url 和 new MvcRouteHandler()
然后为Route属性赋值了MapRoute中传来的参数,这里几个参数是被转化成RouteValueDictionary
我们简单说下RouteValueDictionary,它实际是对 Dictionary<string, object> 进行了包装。他能把object参数解析为key,value形式,并且设置为不区分大小写。下面是上面构造RouteValueDictionary用到的构造方法。
View Code
public RouteValueDictionary(
object values)
{
this._dictionary =
new Dictionary<
string,
object>(StringComparer.OrdinalIgnoreCase);
this.AddValues(values);
}
private
void AddValues(
object values)
{
if (values !=
null)
{
foreach (PropertyDescriptor descriptor
in TypeDescriptor.GetProperties(values))
{
object obj2 = descriptor.GetValue(values);
this.Add(descriptor.Name, obj2);
}
}
}
如可以把 new { controller = "Main", action = "Index", id = UrlParameter.Optional } 这样的参数转化为 key,value,这个类不去做过多的分析了。
接着再看上面的添加路由
Route item = route2; 这一句我没看明白这样写的好处,可能是个人喜好吧。
然后下面接着判断有没有传入 namespaces 参数,就是路由规则限定controller的空间命名(new string[] { "MVCTest.Controllers" })
有果有就存入Route的DataTokens里,我们记得上一篇讲注册area规则,也是存在这个属性里面存入area名称:route.DataTokens["area"] = "Admin";
最后routes.Add(name, item); 把创建好的路由Route加入了RouteCollection集合,也就是RouteTable.Routes那个静态单例的全局路由集合。
打开RouteCollection的Add方法如下
public
void Add(
string name,
RouteBase item)
{
if (item ==
null)
{
throw
new ArgumentNullException(
"
item
");
}
if (!
string.IsNullOrEmpty(name) &&
this._namedMap.ContainsKey(name))
{
throw
new ArgumentException(
string.Format(CultureInfo.CurrentUICulture, RoutingResources.RouteCollection_DuplicateName,
new
object[] { name }),
"
name
");
}
base.Add(item);
if (!
string.IsNullOrEmpty(name))
{
this._namedMap[name] = item;
}
}
里面做了重复路由名称验证。同时发现这里实际的路由参数签名是 RouteBase,RouteBase是路由规则的基类,这个类被定义为 abstract,而且从名字上看也能知道,这个类主要供别的类实现,里面定义了 GetRouteData,GetVirtualPath两个抽象方法。这个抽象类本身自己没有任何功能实现,从这点看,其实这个类应该定义为接口 IRoute 更合理。
通过路由规则基类的两个抽象方法,可以看出,路由规则主要用来做两件事:
一、解析请求url,提取数据。如:/home/index 得到:controller/home,action/index (当然首先要看是否匹配本规则,这个下面再细讲)
提取到的数据会包装成 RouteData ,上面的RouteData routeData = this.RouteCollection.GetRouteData(context); 就是得到提取到的数据。这些下面稍后再细讲。
二、生成url,如我们在页面中 url.Action("Edit", new { id = 5}) //这个我们以后再说
我们先来看下RouteBase
View Code
public
abstract
class RouteBase
{
//
Methods
protected RouteBase();
public
abstract RouteData GetRouteData(HttpContextBase httpContext);
public
abstract VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values);
}
Route是MS帮我们默认实现了RouteBase的类。我们自己也可以实现 RouteBase,然后传给RouteCollection进行路由注册,以后再单独讲如何自己实现,和自己实现的一些利弊。
View Code
public
class Route : RouteBase
{
//
Fields
private ParsedRoute _parsedRoute;
private
string _url;
private
const
string HttpMethodParameterName =
"
httpMethod
";
//
Methods
public Route(
string url, IRouteHandler routeHandler);
public Route(
string url, RouteValueDictionary defaults, IRouteHandler routeHandler);
public Route(
string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler);
public Route(
string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler);
public
override RouteData GetRouteData(HttpContextBase httpContext);
public
override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values);
protected
virtual
bool ProcessConstraint(HttpContextBase httpContext,
object constraint,
string parameterName, RouteValueDictionary values, RouteDirection routeDirection);
private
bool ProcessConstraints(HttpContextBase httpContext, RouteValueDictionary values, RouteDirection routeDirection);
//
Properties
public RouteValueDictionary Constraints {
get;
set; }
public RouteValueDictionary DataTokens {
get;
set; }
public RouteValueDictionary Defaults {
get;
set; }
public IRouteHandler RouteHandler {
get;
set; }
public
string Url {
get;
set; }
}
简单了解一下Route几个属性。
Constraints,保存规则约束,如: new { id = @"^\d{1,2}$" } //这里都用RouteValueDictionary转化了为 key/value 形式。
DataTokens,附加参数,上一篇说到,注册 area 路由就是为这个属性添加area项。还有刚才前面提到的指定controller的空间命名也是放在这里面。item.DataTokens["Namespaces"] = namespaces;
Defaults,这个保存的规则默认值。new { action = "index" }
RouteHandler 上面创建Route己经看到了,是 new MvcRouteHandler(), MvcRouteHandler 是实现了 IRouteHandler。具体作用我们以后会讲到。
Url 规则url
-------------------
Route构造方法没什么说的了,都是赋值。
主要要看Route是如何实现了 RouteBase 的两个抽象方法,来完成路由规则的两项任务的。(解析请求url提取数据 和 生成url)
由于篇幅太长,这个我想放到下篇再细看,我们回到本篇的篇首的
RouteData routeData = this.RouteCollection.GetRouteData(context);
UrlRoutingModule 是调用了 RouteCollection 的 GetRouteData,而不是Route中的GetRouteData。这是因为随意一个请求不可能直接认定它是匹配哪一条路由规则。看下 RouteCollection 的 GetRouteData 方法
View Code
public RouteData GetRouteData(HttpContextBase httpContext)
{
if (httpContext ==
null)
{
throw
new ArgumentNullException(
"
httpContext
");
}
if (httpContext.Request ==
null)
{
throw
new ArgumentException(RoutingResources.RouteTable_ContextMissingRequest,
"
httpContext
");
}
if (!
this.RouteExistingFiles)
{
string appRelativeCurrentExecutionFilePath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
if (((appRelativeCurrentExecutionFilePath !=
"
~/
") && (
this._vpp !=
null)) && (
this._vpp.FileExists(appRelativeCurrentExecutionFilePath) ||
this._vpp.DirectoryExists(appRelativeCurrentExecutionFilePath)))
{
return
null;
}
}
using (
this.GetReadLock())
{
foreach
(RouteBase base2
in
this
)
{
RouteData routeData = base2.GetRouteData(httpContext);
if (routeData !=
null)
{
return routeData;
}
}
}
return
null;
}
上面的代码可以看到,最后是通过 RouteBase 循环了自己所有的路由规则,分别调用我们所有注册在RouteTable.Routes(RouteCollection)的Route的GetRouteData方法,如果为null就进行下一条路由规则,循环匹配路由规则,直到找到一条符合当前请求的路由为止。
从这里我们也可以看到,路由是有优先级的,当一条请求url同时符合两条路由规则时,当找到第一条符合规则的路由时就不往下找了。所以我们在注册路由时一定要考滤到优先级的情况。
我们再看下上面循环用到的 this.GetReadLock() //是一个读写锁,这里不做去分析了。有兴趣的可以详细看下。
我们还注意到上面有这么一段:
if (!
this.RouteExistingFiles)
{
string appRelativeCurrentExecutionFilePath = httpContext.Request.AppRelativeCurrentExecutionFilePath;
if (((appRelativeCurrentExecutionFilePath !=
"
~/
") && (
this._vpp !=
null)) && (
this._vpp.FileExists(appRelativeCurrentExecutionFilePath) ||
this._vpp.DirectoryExists(appRelativeCurrentExecutionFilePath)))
{
return
null;
}
}
RouteCollection 有这么一个属性RouteExistingFiles.当为false时,就检测请求的路径地址是否己经存在文件或目录,如果存在,则直接不走路由了,直接返回null,默认就是false,我们可以实验一下。当然这里是忽略了根目录的,不然默认我们 http://www.xxx.com/ 也不能访问了。
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
new
string[] {
"
MVCTest.Controllers
" }
);
}
这里是默认的路由注册,按理说我们访问 home 时,会去到 home controller 的 index,但是我们在在项目里加一个 home 目录,如下图。
我们再访问:http://localhost:2144/home/ 我们发现,无法找到该资源,也就是检测到home这个目录存在时,就不走路由了。
接下来,我们去把 RouteCollection 的 RouteExistingFiles 设为 true,如下:
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
new
string[] {
"
MVCTest.Controllers
" }
);
routes.RouteExistingFiles =
true
;
}
我们再访问:http://localhost:2144/home/ ,发现可以访问的了,走路由了。
这篇就讲到这吧,主要是介绍了Routing各成员的关系,另外 Route 具体实现了 RouteBase 的 GetRouteData,和GetVirtualPath,还有 RouteData 类 的详细分析,以及UrlRoutingModule 里得到了 RouteData
RouteData routeData = this.RouteCollection.GetRouteData(context);
接下来流程下次再分析。
最后我画了一个简易图来总结我理解的Routing各成员的关系。也是应前一篇文章一个朋友的要求,加点图,画的很不好,见凉,此处也再申明一下,Routing是一个单独的组件,但我这里是把它当做mvc源码的一部分来了解了。
结尾语:
本文会跟着mvc的整个执行流程依次把多数源码分析下去。其中结合了自己的一些见解,如果有什么不正确的地方还请指出。
转载请注明出处,谢谢。