ASP.NET MVC 預設在 Global.asax 所定義的 RegisterRoutes 方法中可以輕易的定義你希望擁有的網址格式,嚴格上來講這並非 ASP.NET MVC 的專利,而是從 ASP.NET 3.5 SP1 就加入的新特性,所以就算是傳統的 ASP.NET Web Form 一樣可以利用 Routing 所帶來的好處,今天我就來講一些 Routing 的觀念與技巧。
快速上手
我先解釋在 ASP.NET MVC 專案中 Global.asax 所定義的 Routing 程式碼,這也是初學者很容易看不懂的地方。
以下文字用來描述上圖標號的部分:
- ASP.NET 執行的起點就在於 HttpApplication 的 Application_Start() 方法,所有 Routing 都會定義在此,其中 RouteTable.Routes 是一個公開的靜態物件,用來儲存所有的 Routing 規則,其物件型別為 RouteCollection。
- 在預設 RegisterRoutes 方法中的 IgnoreRoute 用來定義 不要透過 Routing 處理的網址。
註: IgnoreRoute 擴充方法是 ASP.NET MVC (System.Web.Mvc) 的一部份。 - {resource} 代表一個 路由變數(RouteValue),其名稱為 resource,但在這裡其實取任何名字都可以,這裡只代表一個變數空間 (PlaceHolder) 罷了。總之就是代表一個「位置」,可以放入一個用不到的變數。
- {*pathInfo} 也是代表一個 RouteValue 名稱為 pathInfo,但名稱前面的星號 ( * ) 代表 CatchAll的意思,這個名為 pathInfo 的 RouteValue 會是完整的 PATH INFO 扣除 標號 3 比對到的網址。例如:若網址是 /TEST.axd/a/b/c/d 則 pathInfo 所得到的值為 a/b/c/d,如果沒加上星號 ( * ) 該 pathInfo 就會等於 a 而已。而在這裡其實取任何名字都可以,這裡也只是代表一個變數的位置。
- MapRoute 則是最常用來定義 Routing 規則的擴充方法。
註: MapRoute 擴充方法是 ASP.NET MVC (System.Web.Mvc) 的一部份。 - 定義 Route 名稱,這個名稱雖然少用,但是開發到較為進階的 Routing 技巧時會用到。
- 定義 網址格式 與每個 網址段落(Path Segment) 的 RouteValue 參數名稱。
注意: 該網址不能以斜線 ( / ) 開頭。 - 定義各 RouteValue 參數的預設值
基本觀念
- Routing (System.Web.Routing) 是 ASP.NET 3.5 SP1 的一部份,ASP.NET MVC 只是使用而已
- RouteTable.Routes 是一個公開的靜態物件,在整個 HttpApplication 生命週期裡都會固定存在!
- 由於 RouteTable.Routes 是一個公開的靜態物件,所以在不用重新編譯網站的情況下是有可能動態調整 RouteTable.Routes 內容的。詳見:Editable Routes 與 Editable Routes Using App_Code
- IgnoreRoute 與 MapRoute 這兩個擴充方法 (ExtensionMethod) 是 System.Web.Mvc 命名空間中所新增的 RouteCollectionExtensions 類別所提供的擴充方法,為了方便撰寫 Routing 規則之用。
- 在 Routing 規則集之中,第一個被比對成功的規則就會直接被採用,此原則跟 Firewall Rules 類似
路由開發技巧
技巧 1:替 Routing 網址設立條件限制
由於 ASP.NET MVC 有個很重要的特性:以習慣取代設定 (Convention over Configuration),所以當我們從網址路徑對應到 Controller 的過程中,網址列上的路由變數會自動對應到 Action 方法的參數中,例如我們在 Global.asax.cs 檔案中有以下路由定義:
routes.MapRoute( "Product", "Product/{productId}", new {controller="Product", action="Details"} );
而我們的 Action 方法的內容如下:
public ActionResult Details(int productId) { return View(); }
因為我們 Action 方法裡的參數是 int 型別,所以傳入的 productId 路由變數必須要是「整數」,如果不是整數的話,就會導致 The parameters dictionary contains a null entry for parameter ‘productId‘ of non-nullable type ‘System.Int32‘ for method ‘System.Web.ActionResult Details(Int32)‘ in ... 的錯誤發生。
要避免這種無效的網址路由比對,我們可以在定義路由時多下一些限制條件,來進一步比對 網址段落(PathSegment) 是否符合 路由變數(RouteValue) 的格式要求,我們以上述例子來進一步修改路由定義,如下程式範例,我們多增加了一個參數傳入 MapRoute 參數,其中 \d+ 是 正規表示式(RegEx) 樣式:
routes.MapRoute( "Product", "Product/{productId}", new {controller="Product", action="Details"}, new {productId = @"\d+" } );
注意:這裡的 RegEx 樣式在比對時會自動幫你加上 ^ 開頭與 $ 結尾,也代表這個樣式一定會比對完整的 網址段落(Path Segment)。
如此一來,當網址輸入的格式是 /Product/CellPhone 的時候,就不會比對到 "Product" 這一條路由規則。
技巧 2:自訂 Routing 條件限制
一般情況下透過 RegEx 正規表示式來定義 RouteValue 條件限制已經很足夠,但若遇到需要較為複雜的判斷情況時,就有可能要自訂 Routing 條件限制,自訂的方法是透過實做 IRouteConstraint 介面的方式來完成,該介面只有一個 Match 方法需要實做,以下是該方法的定義:
bool Match( HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection )
有了這些傳入的參數,你將可以取得許多在網址變數比對時可能得到的各種資訊,再拿這些取得的資料進行判斷,就可以有成千上百種條件的限制組合,任你怎樣寫都可以。例如你想設定一組只有在 Localhost 本機執行網站時才能使用的路由限制(Route Constraint),我們可以透過 httpContext.Request.IsLocal 判斷來源電腦是否來自於本機,這樣我們可以寫出以下程式碼:
using System.Web; using System.Web.Routing; namespace MvcApplication1.Constraints { public class LocalhostConstraint : IRouteConstraint { public bool Match (HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { return httpContext.Request.IsLocal; } } }
那麼我們在 Global.asax.cs 裡的路由規則又應該如何定義呢?請看以下程式碼:
routes.MapRoute( "Admin", "Admin/{action}", new { controller="Admin" }, new { isLocal=new LocalhostConstraint() } );
眼尖的人可能會發現我們在限制條件的地方使用了一個 isLocal 變數,但是該變數根本沒有出現在網址格式 之中出現過,但為什麼能用呢?這是要限制誰呢?真的有一個 isLocal 這個 RouteValue 變數嗎?
我相信初學者看到這些程式碼很可能會有這些疑問,因為當時的我看到這段 Code 也質疑了一下,事實上自訂路由限制套用在路由定義時,其匿名變數的屬性名稱一點也不重要,所有的判斷都是寫在自訂路由限制的程式碼裡,所以上述的路由定義如果我修改成以下,還是可以正常運作的,你自己取名的時候只要自己覺得好記、好懂即可:
routes.MapRoute( "Admin", "Admin/{action}", new { controller="Admin" }, new { OnlyLocalhostCanApply=new LocalhostConstraint() } );
技巧 3:讓 ASP.NET MVC 與 ASP.NET Web Form 和平共處
基於 ASP.NET MVC 與 ASP.NET Web Form 的運作方式不同,所以通常這兩種架構下的程式在預設的情況下都能正常的運作,只有在某些特殊的設定下才會兩邊互相干擾,當你網站部署時遇到互相干擾的狀況時,就可以套用以下 Routing 技巧,設定特定一些路徑不要走 ASP.NET MVC 的執行管線。
以下是一些過濾掉路由規則定義的程式碼範例:
- 忽略所有 *.aspx 的網址路徑
routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
- 忽略所有在 Page 目錄下的所有程式與檔案 ( 請注意,這是忽略 Routing 而已,改由 IIS 來判斷要用何種 Handler 來處理這次 HTTP 要求 )
routes.IgnoreRoute("Page/{*pathInfo}");
- 忽略所有在 Page 目錄下的所有檔案 ( 另一種寫法 )
routes.Add(new Route("Page/{*pathInfo}", new StopRoutingHandler()));
另外還有一點也值得注意,也就是 Routing 的動作其實是在執行 HttpHandler 執行之前,他是一個專門用來分配 Request 的模組,所以可以透過 Routing 的定義與設定來決定到底後續要執行 ASP.NET MVC 還是 ASP.NET Web Form,在 IIS7 的 HTTP Request 的執行順序中,UrlRoutingModule (Routing module) 的執行順位如下圖示,執行事件名稱為 Resolve Cache 這一步之後(PostResolveRequestCache):
以下一些參考連結可以幫助你學習如何讓 ASP.NET Web Form 的網頁也套用 Routing 規則:
- Using Routing With WebForms
- URL Routing with ASP.NET 4 Web Forms (VS 2010 and .NET 4.0 Series)
- Extreme ASP.NET - Routing with ASP.NET Web Forms
技巧 4:讓 Routing 也對已存在的檔案做處理 (將所有網址都透過 Routing 進行比對)
當 HTTP 要求進來後,在 ASP.NET Routing 在預設的情況下,只要當輸入的網址路徑可以比對出在實體的檔案系統中的檔案,就會直接跳過 Routing 機制,這也是為什麼 ASP.NET MVC 與 ASP.NET Web Form 可以和平共處的原因之一,因為 ASP.NET Web Form 一定會有個 aspx 檔案 (或其他檔案類型),當 HTTP 要求進來後,就會立即找到有個實體檔案在,這時就會直接跳過 ASP.NET Routing 機制。
這樣的設計其實在大部分的環境下都可以正常運作,但在一些特殊的設計裡,你可能會想要求所有 HTTP 要求都必須經過 ASP.NET Routing 機制的比對才行 (例如你想藉由 ASP.NET Routing 實做權限管理),這時就可以在 Global.asax.cs 檔案裡的 RegisterRoutes 方法加上以下程式碼即可:
routes.RouteExistingFiles = true;
如下圖示:
這樣的設計當然也會帶來一些副作用,因為當你真的有實體檔案要能直接透過 IIS 回應的時候,就必須針對特定的檔案類型或路徑設定 IgnoreRoute 或 StopRoutingHandler (本文技巧 3 有提到)。