ASP.NET路由系统实现原理:HttpHandler的动态映射
我们知道一个请求最终通过一个具体的HttpHandler进行处理,而我们熟悉的用于表示一个Web页面的Page对象就是一个HttpHandler,被用于处理基于某个.aspx文件的请求。我们可以通过HttpHandler的动态映射来实现请求地址与物理文件路径之间的分离。实际上ASP.NET路由系统就是采用了这样的实现原理。如下图所示,ASP.NET路由系统通过一个注册到当前应用的自定义HttpModule对所有的请求进行拦截,并通过对请求的分析为之动态匹配一个用于处理它的HttpHandler。HttpHandler对请求进行处理后将相应的结果写入HTTP回复以实现对请求的相应。
目录
一、UrlRoutingModule
一、UrlRoutingModule
二、PageRouteHandler V.S. MvcRouteHandler
三、ASP.NET路由系统扩展
实例演示:通过自定义Route对ASP.NET路由系统进行扩展
上图所示的作为请求拦截器的HttpModule类型为UrlRoutingModule。如下面的代码片断所示,UrlRoutingModule对请求的拦截是通过注册表示当前应用的HttpApplication的PostResolveRequestCache事件实现的。
1: public class UrlRoutingModule : IHttpModule
2: {
3: //其他成员
4: public RouteCollection RouteCollection { get; set; }
5: public void Init(HttpApplication context)
6: {
7: context.PostResolveRequestCache += new EventHandler(this.OnApplicationPostResolveRequestCache);
8:
9: }
10: private void OnApplicationPostResolveRequestCache(object sender, EventArgs e);
11: }
UrlRoutingModule具有一个类型为RouteCollection的RouteCollection属性,在默认的情况下引用这通过RouteTable的静态属性Routes表示的全局路由表。针对请求的HttpHandler的动态映射就实现在OnApplicationPostResolveRequestCache方法中,具体的实现逻辑非常简单:通过HttpApplication获得但前的HTTP上下文,并将其作为参数调用RouteCollection的GetRouteData方法得到一个RouteData对象。
通过RouteData的RouteHandler属性可以得到一个实现了IRouteHandler的路由处理器对象,而调用后者的GetHttpHandler方法直接可以获取对应的HttpHandler对象,而我们需要映射到当前请求的就是这么一个 HttpHandler。下面的代码片断基本上体现了定义在UrlRoutingModule的OnApplicationPostResolveRequestCache方法中的动态HttpHandler映射逻辑。
1: public class UrlRoutingModule : IHttpModule
2: {
3: //其他成员
4: private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
5: {
6: HttpContext context = ((HttpApplication)sender).Context;
7: HttpContextBase contextWrapper = new HttpContextWrapper(context);
8: RouteData routeData = this.RouteCollection.GetRouteData(contextWrapper);
9: RequestContext requestContext = new RequestContext(contextWrapper, routeData);
10: IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext);
11: context.RemapHandler(handler);
12: }
13: }
二、 PageRouteHandler V.S. MvcRouteHandler
通过前面的介绍我们知道对于调用RouteCollection的GetRouteData获得的RouteData对象,其RouteHandler来源于匹配的Route对象。对于通过调用RouteCollection的MapPageRoute方法注册的Route来说,它的RouteHandler是一个类型为PageRouteHandler对象。
由于调用MapPageRoute方法的目的在于实现请求地址与某个.aspx页面文件之间的映射,所以我们最终还是要创建的Page对象还处理相应的请求,所以PageRouteHandler的GetHttpHandler方法最终返回的就是针对映射页面文件路径的Page对象。此外,MapPageRoute方法中还可以控制是否对物理文件地址实施授权,而授权在返回Page对象之前进行。
定义在PageRouteHandler中的HttpHandler获取逻辑基本上体现在如下的代码片断中,两个属性VirtualPath和CheckPhysicalUrlAccess表示页面文件的地址和是否需要对物理文件地址实施URL授权,它们在构造函数中被初始化,而最终来源于调用RouteCollection的MapPageRoute方法传入的参数。
1: public class PageRouteHandler : IRouteHandler
2: {
3: public bool CheckPhysicalUrlAccess { get; private set; }
4: public string VirtualPath { get; private set; }
5: public PageRouteHandler(string virtualPath, bool checkPhysicalUrlAccess)
6: {
7: this.VirtualPath = virtualPath;
8: this.CheckPhysicalUrlAccess = checkPhysicalUrlAccess;
9: }
10: public IHttpHandler GetHttpHandler(RequestContext requestContext)
11: {
12: if (this.CheckPhysicalUrlAccess)
13: {
14: //Check Physical Url Access
15: }
16: return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(this.VirtualPath, typeof(Page))
17: }
18: }
ASP.NET MVC的Route对象是通过调用RouteCollection的扩展方法MapRoute方法进行注册的,它对应的RouteHandler是一个类型为MvcRouteHandler的对象。如下面的代码片断所示,MvcRouteHandler用于获取处理当前请求的HttpHandler是一个MvcHandler对象。MvcHandler实现对Controller的激活、Action方法的执行以及对请求的相应,毫不夸张地说,整个MVC框架实现在MvcHandler之中。
1: public class MvcRouteHandler : IRouteHandler
2: {
3: //其他成员
4: public IHttpHandler GetHttpHandler(RequestContext requestContext)
5: {
6: return new MvcHandler(requestContext)
7: }
8: }
三、 ASP.NET路由系统扩展
到此为止我们已经对ASP.NET的路由系统的实现进行了详细介绍,总的来说,整个路由系统是通过对HttpHandler的动态注册的方式来实现的。具体来说,UrlRoutingModule通过对代表Web应用的HttpApplication的PostResolveRequestCache事件的注册实现了对请求的拦截。对于被拦截的请求,UrlRoutingModule利用注册的路由表对其进行匹配和解析,进而得到一个包含所有路由信息的RouteData对象。最终借助该对象的RouteHandler创建出相应的HttpHandler映射到当前请求。从可扩展性的角度来讲,我们可以通过如下三种方式来实现我们需要的路由方式。
- 通过集成抽象类RouteBase创建自定义Route定制路由逻辑。
- 通过实现接口IRouteHandler创建自定义RouteHandler定制HttpHandler提供机制。
- 通过实现IHttpHandler创建自定义HttpHandler来对请求处理。
实例演示:通过自定义Route对ASP.NET路由系统进行扩展
定义在ASP.NET路由系统中默认的路由类型Route建立了定义成文本模板的URL模式与某个物理文件之间的映射,如果我们对WCF REST有一定的了解,应该知道其中也有类似的实现。具体来说,WCF REST借助于System.UriTemplate这个对象实现了同样定义成某个文本模板的URI模式与目标操作之间的映射。篇幅所限,我们不能对WCF REST的UriTemplate作详细的介绍,有兴趣的读者可以参考《UriTemplate、UriTemplateTable与WebHttpDispatchOperationSelector》。[源代码从这里下载]
我们创建一个新的ASP.NET Web应用,并且添加针对程序集System.ServiceModel.dll的引用(UriTemplate定义在该程序集中),然后创建如下一个针对UriTemplate的路由类型UriTemplateRoute。
1: public class UriTemplateRoute:RouteBase
2: {
3: public UriTemplate UriTemplate { get; private set; }
4: public IRouteHandler RouteHandler { get; private set; }
5: public RouteValueDictionary DataTokens { get; private set; }
6:
7: public UriTemplateRoute(string template, string physicalPath, object dataTokens = null)
8: {
9: this.UriTemplate = new UriTemplate(template);
10: this.RouteHandler = new PageRouteHandler(physicalPath);
11: if (null != dataTokens)
12: {
13: this.DataTokens = new RouteValueDictionary(dataTokens);
14: }
15: else
16: {
17: this.DataTokens = new RouteValueDictionary();
18: }
19: }
20: public override RouteData GetRouteData(HttpContextBase httpContext)
21: {
22: Uri uri = httpContext.Request.Url;
23: Uri baseAddress = new Uri(string.Format("{0}://{1}", uri.Scheme, uri.Authority));
24: UriTemplateMatch match = this.UriTemplate.Match(baseAddress, uri);
25: if (null == match)
26: {
27: return null;
28: }
29: RouteData routeData = new RouteData();
30: routeData.RouteHandler = this.RouteHandler;
31: routeData.Route = this;
32: foreach (string name in match.BoundVariables.Keys)
33: {
34: routeData.Values.Add(name,match.BoundVariables[name]);
35: }
36: foreach (var token in this.DataTokens)
37: {
38: routeData.DataTokens.Add(token.Key, token.Value);
39: }
40: return routeData;
41: }
42: public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
43: {
44: Uri uri = requestContext.HttpContext.Request.Url;
45: Uri baseAddress = new Uri(string.Format("{0}://{1}", uri.Scheme, uri.Authority));
46: Dictionary<string, string> variables = new Dictionary<string, string>();
47: foreach(var item in values)
48: {
49: variables.Add(item.Key, item.Value.ToString());
50: }
51:
52: //确定段变量是否被提供
53: foreach (var name in this.UriTemplate.PathSegmentVariableNames)
54: {
55: if(!this.UriTemplate.Defaults.Keys.Any(key=> string.Compare(name,key,true) == 0) &&
56: !values.Keys.Any(key=> string.Compare(name,key,true) == 0))
57: {
58: return null;
59: }
60: }
61: //确定查询变量是否被提供
62: foreach (var name in this.UriTemplate.QueryValueVariableNames)
63: {
64: if(!this.UriTemplate.Defaults.Keys.Any(key=> string.Compare(name,key,true) == 0) &&
65: !values.Keys.Any(key=> string.Compare(name,key,true) == 0))
66: {
67: return null;
68: }
69: }
70:
71: Uri virtualPath = this.UriTemplate.BindByName(baseAddress, variables);
72: string strVirtualPath = virtualPath.ToString().ToLower().Replace(baseAddress.ToString().ToLower(),"");
73: VirtualPathData virtualPathData = new VirtualPathData(this, strVirtualPath);
74: foreach (var token in this.DataTokens)
75: {
76: virtualPathData.DataTokens.Add(token.Key, token.Value);
77: }
78: return virtualPathData;
79: }
80: }
如上面的代码片断所示,UriTemplateRoute具有UriTemplate、DataTokens和RouteHandler三个只读属性,前两个通过构造函数的参数进行初始化,后者则是在构造函数中创建的PageRouteHandler对象。
用于对入栈请求进行匹配判断的GetRouteData方法中,我们解析出基于应用的基地址并量连同请求地址作为参数调用UriTemplate的Match方法,如果返回的UriTemplateMatch对象不为Null,则意味着URL模板的模式与请求地址匹配。在匹配的情况下我们创建并返回相应的RouteData对象,否则直接返回Null。
在用于生成出栈URL的GetVirtualPath方法中,我们通过定义在URL模板中的模板(包括变量名包含在属性PathSegmentVariableNames的路径段变量和包含在QueryValueVariableNames属性的查询变量)是否在提供的RouteValueDictionary字段或者默认变量列表(通过属性Defaults表示)从判断URL模板是否与提供的变量列表匹配。在匹配的情况下通过调用UriTemplate的BindByName方法得到一个完整的Uri。由于该方法返回的是相对路径,所以我们需要将应用基地址剔除并最终创建并返回一个VirtualPathData对象。如果不匹配,则直接返回Null。
在创建的Global.asax文件中我们采用如下的代码对我们自定义的UriTemplateRoute进行注册,选用的场景还是我们上面采用的天气预报的例子。我个人具有基于UriTemplate的URI模板比针对Route的URL模板更好用,其中一点就是它在定义默认值方法更为直接。如下面的代码片断所示,我们直接将默认值定义在模板中(("{areacode=010}/{days=2})。
1: public class Global : System.Web.HttpApplication
2: {
3: protected void Application_Start(object sender, EventArgs e)
4: {
5: UriTemplateRoute route = new UriTemplateRoute("{areacode=010}/{days=2}",
6: "~/Weather.aspx", new { defualtCity = "BeiJing", defaultDays = 2});
7: RouteTable.Routes.Add("default", route);
8: }
9: }
在注册的路由对应的目标页面Weather.aspx的后台代码中,我们定义了如下一个GenerateUrl根据指定的区号(areacode)和预报天数(days)创建一个Url,而Url的生成直接通过调用RouteTable的Routes属性的GetVirtualPathData方法完成。生成的URL连同当前页面的RouteData的属性通过如下所示的HTML输出来。
1: <body>
2: <form id="form1" runat="server">
3: <div>
4: <table>
5: <tr>
6: <td>Router:</td>
7: <td><%=RouteData.Route != null? RouteData.Route.GetType().FullName:"" %></td>
8: </tr>
9: <tr>
10: <td>RouteHandler:</td>
11: <td><%=RouteData.RouteHandler != null? RouteData.RouteHandler.GetType().FullName:"" %></td>
12: </tr>
13: <tr>
14: <td>Values:</td>
15: <td>
16: <ul>
17: <%foreach (var variable in RouteData.Values)
18: {%>
19: <li><%=variable.Key%>=<%=variable.Value%></li>
20: <% }%>
21: </ul>
22: </td>
23: </tr>
24: <tr>
25: <td>DataTokens:</td>
26: <td>
27: <ul>
28: <%foreach (var variable in RouteData.DataTokens)
29: {%>
30: <li><%=variable.Key%>=<%=variable.Value%></li>
31: <% }%>
32: </ul>
33: </td>
34: </tr>
35: <tr>
36: <td>Generated Url:</td>
37: <td>
38: <%=GenerateUrl("0512",3)%>
39: </td>
40: </tr>
41: </table>
42: </div>
43: </form>
44: </body>
由于注册的URL模板所包含的段均由具有默认值的变量构成,所以当我们请求根地址时,会自动路由到Weather.aspx。下图是我们在浏览器访问应用根目录的截图,上面显示了我们注册的UriTemplateRoute生成的RouteData的信息和生成URL(/0512/3)。