我們知道一個請求最終通過一個具體的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)。


