asp.net Mvc学习之URL路由
Asp.Net MVC的请求的执行过程粗略的来看大致是这样的:
1 WebServer 接收来自的客户端的Request(请求)。
2 Web Application在第一次运行的时候(Application_Start())根据其中的设置代码会创建一个RouteTable(路由表)实现URL到处理程序之间的映射。
3 UrlRotingModule模块解析该请求的URL,并选择相关的URL路由。
4 MvcHandler对象来处理该URL路由,创建要执行的控制器(Controller )。
5 执行Controller(即调用指定的执行方法)。
6 返回处理结果(执行View()方法,返回视图到浏览器)。
那么我们首先来深入了解一下URL路由。其实URL路由是ASP.NET 3.5 MVC框架中独立出来的一个功能,也就是说不仅仅在MVC中,即使是在传统的WebForm也可以使用它。
------------------------------------------------------------------------------------------------------------------------------------------
一 首先,URL路由是如何加入到HttpApplication处理管道中来的?
我们注意到MVC项目的WebConfig中有这么一个配置项
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
那么UrlRoutingModule必然是处理URL路由的一个HttpModule了,将System.Web.Routing进行反编译,并找到UrlRoutingModule这个类我们可以看到它在Init方法中注册了下面两个事件
application.PostResolveRequestCache += new EventHandler(this.OnApplicationPostResolveRequestCache);
application.PostMapRequestHandler += new EventHandler(this.OnApplicationPostMapRequestHandler);
在此我们回顾下HttpApplication处理管线中的各种事件(已省略与本文无关的事件)
1 请求验证,检查浏览器发送的信息,包括确定是否包含潜在的恶意标记等。
2 如果WebConfig中配置了UrlMappingsSection,则执行URL映射
3 引发BeginRequest事件
......
......
9 引发PostResolveRequestCache事件
10 引发MapRequestHandler事件:
根据所请求资源文件的扩展名(在应用程序的配置文件中映射),选择对应实现IHttpHandler接口的处理类。如果请求是aspx,并且需要对该页进行编译,则asp.net会在获取该页面实例之前对其进行编译。
11 引发PostMapRequestHandler事件
......
......
15 调用第10步选择的IHttpHandler的ProcessRequest方法(或异步版的BeginProcessRequest)
......
......
22引发EndRequest事件
上面黄色部分标明的则是UrlRoutingModule所注册的事件
这两个事件最开始都对HttpContext进行再次封装()
【关于下文要提到的Route,RouteConllection,RouteData等的简单结构说明】
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");
}
}
(1) 根据Url在路由表中查找匹配的路由获得RouteData,即:
遍历RouteCollection集合,调用集合中Route(路由)的GetRouteData方法,如果GetRouteData方法返回的RouteData对象不为空,则立即返回,这也就是说明了路由是按照路由设置的次序解析的,就算一个URL可以匹配多个路由,解析过程中一旦发现有匹配的路由就不会再进行后面的路由检索了,直接结束路由的解析过程。
(2) 如果最终获取到的RouteData不为空,获取RouteData的RouteHandler属性,RouteHandler是实现了IRouteHandler接口的,只有一个方法,那就是GetHttpHandler,此Handler(MvcRouteHandler)用于处理URL路由
(3) 如果这个RouteHandler的类型不是StopRoutingHandler
1) 封装一个RequestContext类,此类只有两个属性,一个是之前的RouteData,还有就是封装过后的context(HttpContextBase)
2) 调用之前(2)中RouteHandler中的GetHttpHandler方法传入上一步的RequestContext参数将得到的HttpHandler,将其与当前的请求路径(context.Request.Path)一并封装到RequestData对象中,并将RequestData存入context
3) 重写路径,指向UrlRouting.axd文件(context.RewritePath("~/UrlRouting.axd")),此文件是类似于aspx,实现了IHttpHandler接口。
2 PostMapRequestHandler中做了如下处理:
在此事件中要替换掉context中的handler, 从context中获取到在之前这个PostResolveRequestCache事件中存入的RequestData,从RequestData获取OriginalPath,将开始从写路径到UrlRouting.axd改回最开始状态(即OriginalPath)让输出路径和输入路径相同,同样设置context的Handler为之前存入RequestData的HttpHandler。
这个Handler在配置路由表,往RouteConllection中存入Route实例化这个Route时被传入
3 HttpApplication处理管道后面事件中将会调用此Handler(MvcHandler)的ProcessRequest方法。
写到这里我产生了一个疑问??
为什么在PostResolveRequestCache事件中将得到RequestData并存入Context 又在后面的PostMapRequestHandler事件中将其取出得到开始存入的Handler对象以及原始URL,并把重写的路径改回原始的URL再设置Context的Handler属性,而不是这些过程全部都在PostMapRequestHandler这个事件中处理了,这么一存一放不是多此一举吗?
至此我们再来研究下位于PostResolveRequestCache事件与PostMapRequestHandler事件之间的MapRequestHandler事件:
之前的说明是:
根据所请求资源文件的扩展名(在应用程序的配置文件中映射),选择对应实现IHttpHandler接口的处理类。如果请求是aspx,并且需要对该页进行编译,则asp.net会在获取该页面实例之前对其进行编译。
继续查资料可以知道
此事件只在IIS7.0的集成模式,且.Net Framework版本大于等于3.0的情况下才会触发,由于asp.net mvc请求的路径并不会对应一个文件,所以在此处会报错,MS为了使处理模块能够在iis7中实现路由,则采取了这么一种简单的解决办法。先把路径指向~/UrlRouting.axd,在此事件中会设置一个UrlRouting.axd类型的Handler避免报错,并在下一步事件中替换掉此处的Handler再把~/UrlRouting.axd这个路径给改回来。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
关于路由的使用:
(对于已存在路径的文件想要其不被路由:
RouteTable.Routes.RouteExistingFiles = false;)
一般通过Global.asax文件,在Application_Start()方法设置
添加路由
Example:
routes.Add("Default", new Route
(
"{controller}/{action}/{id}",
new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" }, { "id", UrlParameter.Optional } },
new MvcRouteHandler()
));
常量 控制器名 对应的方法 变量username 变量password
routes.Add("Register", new Route
(
"Register/{controller}/{action}/{username}/{password}",
new RouteValueDictionary { { "controller", "Register" }, { "action", "RegisterUser" } },
));
设置默认值
由于发布System.Web.Routing程序集之后MS也在不断的改进,MS感觉上述路由的设置比较麻烦,但是此程序集又已经发布了, 所以使用到.net 3.5 framework 新特性,扩展方法,即可以在原有类别中添加新的实现方法,从而实现新的功能。
位于System.Web.Mvc中的RouteCollectionExtensions类就是一个静态类,其中定义的方法就是扩展方法
针对于路由集合类RouteCollection扩展了两类方法
IgnoreRoute()
MapRoute()
对于MapRoute方法可以将上述代码简化为
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "", action = "", id = UrlParameter.Optional }
);
routes.MapRoute(
"Register ",
"Register/{controller}/{action}/{username}/{password}",
new { controller = "Register", action = "RegisterUser" }
);
忽略路由
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
在上面的路由设置其中的路由名称是可选的参数,路由名称可以用来生成URL路由,但其在路由解析中并没有作用
但是如果在视图中生成相关的路由链接,可以直接指定路由名称,加快检索速度,使用指定路由的好处还有可以不必知名路由的其他参数,例如控制器,控制方法等
建议: 将常用的路由存放在路由表最前端
自定义路由约束
实现一个约束类:
public class IDRouteConstraint: IRouteConstraint
{
#region IRouteConstraint Members
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if ((routeDirection == RouteDirection.IncomingRequest)
&& (parameterName.ToLower(CultureInfo.InvariantCulture) == "id"))
{
long id;
if (long.TryParse(values["id"].ToString(), out id))
{
if (id < 1000000 && id >= 0)
{
return true;
}
}
}
return false;
}
#endregion
}
在路由中配置
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new
{
controller = "Home", action = "Index", id="1001"
},
new
{
id = new IDRouteConstraint()
}
);