ASP.NET路由系统实现原理:HttpHandler的动态映射

简介:

我们知道一个请求最终通过一个具体的HttpHandler进行处理,而我们熟悉的用于表示一个Web页面的Page对象就是一个HttpHandler,被用于处理基于某个.aspx文件的请求。我们可以通过HttpHandler的动态映射来实现请求地址与物理文件路径之间的分离。实际上ASP.NET路由系统就是采用了这样的实现原理。如下图所示,ASP.NET路由系统通过一个注册到当前应用的自定义HttpModule对所有的请求进行拦截,并通过对请求的分析为之动态匹配一个用于处理它的HttpHandler。HttpHandler对请求进行处理后将相应的结果写入HTTP回复以实现对请求的相应。

clip_image002

目录
一、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)。

clip_image004



作者:蒋金楠
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
1月前
|
关系型数据库 C# 数据库
.NET 8.0 开源在线考试系统(支持移动端)
【10月更文挑战第27天】以下是适用于 .NET 8.0 的开源在线考试系统(支持移动端)的简介: 1. **基于 .NET Core**:跨平台,支持多种数据库,前后端分离,适用于多操作系统。 2. **结合 Blazor**:使用 C# 开发 Web 应用,支持响应式设计,优化移动端体验。 3. **基于 .NET MAUI**:跨平台移动应用开发,一套代码多平台运行,提高开发效率。 开发时需关注界面设计、安全性与稳定性。
|
1月前
|
Windows
.NET 隐藏/自定义windows系统光标
【10月更文挑战第20天】在.NET中,可以使用`Cursor`类来控制光标。要隐藏光标,可将光标设置为`Cursors.None`。此外,还可以通过从文件或资源加载自定义光标来更改光标的样式。例如,在表单加载时设置`this.Cursor = Cursors.None`隐藏光标,或使用`Cursor.FromFile`方法加载自定义光标文件,也可以将光标文件添加到项目资源中并通过资源管理器加载。这些方法适用于整个表单或特定控件。
|
2月前
|
JSON 安全 数据安全/隐私保护
从0到1搭建权限管理系统系列三 .net8 JWT创建Token并使用
【9月更文挑战第22天】在.NET 8中,从零开始搭建权限管理系统并使用JWT(JSON Web Tokens)创建Token是关键步骤。JWT是一种开放标准(RFC 7519),用于安全传输信息,由头部、载荷和签名三部分组成。首先需安装`Microsoft.AspNetCore.Authentication.JwtBearer`包,并在`Program.cs`中配置JWT服务。接着,创建一个静态方法`GenerateToken`生成包含用户名和角色的Token。最后,在控制器中使用`[Authorize]`属性验证和解析Token,从而实现身份验证和授权功能。
155 3
|
3月前
|
设计模式 存储 前端开发
揭秘.NET架构设计模式:如何构建坚不可摧的系统?掌握这些,让你的项目无懈可击!
【8月更文挑战第28天】在软件开发中,设计模式是解决常见问题的经典方案,助力构建可维护、可扩展的系统。本文探讨了.NET中三种关键架构设计模式:MVC、依赖注入与仓储模式,并提供了示例代码。MVC通过模型、视图和控制器分离关注点;依赖注入则通过外部管理组件依赖提升复用性和可测性;仓储模式则统一数据访问接口,分离数据逻辑与业务逻辑。掌握这些模式有助于开发者优化系统架构,提升软件质量。
58 5
|
3月前
|
C# Windows 开发者
超越选择焦虑:深入解析WinForms、WPF与UWP——谁才是打造顶级.NET桌面应用的终极利器?从开发效率到视觉享受,全面解读三大框架优劣,助你精准匹配项目需求,构建完美桌面应用生态系统
【8月更文挑战第31天】.NET框架为开发者提供了多种桌面应用开发选项,包括WinForms、WPF和UWP。WinForms简单易用,适合快速开发基本应用;WPF提供强大的UI设计工具和丰富的视觉体验,支持XAML,易于实现复杂布局;UWP专为Windows 10设计,支持多设备,充分利用现代硬件特性。本文通过示例代码详细介绍这三种框架的特点,帮助读者根据项目需求做出明智选择。以下是各框架的简单示例代码,便于理解其基本用法。
179 0
|
3月前
|
开发框架 NoSQL .NET
使用 Asp.net core webapi 集成配置系统,提高程序的灵活和可维护性
使用 Asp.net core webapi 集成配置系统,提高程序的灵活和可维护性
|
5月前
|
开发框架 前端开发 .NET
LIMS(实验室)信息管理系统源码、有哪些应用领域?采用C# ASP.NET dotnet 3.5 开发的一套实验室信息系统源码
集成于VS 2019,EXT.NET前端和ASP.NET后端,搭配MSSQL 2018数据库。系统覆盖样品管理、数据分析、报表和项目管理等实验室全流程。应用广泛,包括生产质检(如石化、制药)、环保监测、试验研究等领域。随着技术发展,现代LIMS还融合了临床、电子实验室笔记本和SaaS等功能,以满足复杂多样的实验室管理需求。
83 3
LIMS(实验室)信息管理系统源码、有哪些应用领域?采用C# ASP.NET dotnet 3.5 开发的一套实验室信息系统源码
|
4月前
|
开发框架 监控 NoSQL
.NET开源的实时应用监控系统 - WatchDog
.NET开源的实时应用监控系统 - WatchDog
|
4月前
|
SQL JavaScript 安全
基于.NET开源跨平台的文档管理系统
基于.NET开源跨平台的文档管理系统
134 0
|
2月前
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
43 7
下一篇
无影云桌面