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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM