﻿# ASP.NET MVC 開發心得分享 (21)：Routing 觀念與技巧

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 程式碼，這也是初學者很容易看不懂的地方。

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/0d22ef8760cdf018cb6b-image_984.png)

以下文字用來描述上圖標號的部分：

1.  ASP.NET 執行的起點就在於 [HttpApplication](http://msdn.microsoft.com/zh-tw/library/system.web.httpapplication\(v=VS.90\).aspx) 的 Application\_Start() 方法，所有 Routing 都會定義在此，其中 [RouteTable](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.aspx).[Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 是一個**公開**的**靜態物件**，用來儲存所有的 Routing 規則，其物件型別為 [RouteCollection](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routecollection.aspx)。
2.  在預設 RegisterRoutes 方法中的 [IgnoreRoute](http://msdn.microsoft.com/en-us/library/dd470170.aspx) 用來定義 **不要透過 Routing 處理的網址**。  
    註: [IgnoreRoute](http://msdn.microsoft.com/en-us/library/dd470170.aspx) 擴充方法是 ASP.NET MVC (System.Web.Mvc) 的一部份。
3.  {resource} 代表一個 **路由變數**(**RouteValue**)，其名稱為 resource，但在這裡其實取任何名字都可以，這裡只代表一個變數空間 (PlaceHolder) 罷了。總之就是代表一個「位置」，可以放入一個用不到的變數。
4.  {\*pathInfo} 也是代表一個 RouteValue 名稱為 pathInfo，但名稱前面的星號 ( \* ) 代表 CatchAll 的意思，這個名為 pathInfo 的 RouteValue 會是完整的 PATH INFO 扣除 標號 3 比對到的網址。例如：若網址是 /TEST.axd/a/b/c/d 則 pathInfo 所得到的值為 a/b/c/d，如果沒加上星號 ( \* ) 該 pathInfo 就會等於 a 而已。而在這裡其實取任何名字都可以，這裡也只是代表一個變數的位置。
5.  [MapRoute](http://msdn.microsoft.com/en-us/library/system.web.mvc.routecollectionextensions.maproute.aspx) 則是最常用來定義 Routing 規則的擴充方法。  
    註: [MapRoute](http://msdn.microsoft.com/en-us/library/system.web.mvc.routecollectionextensions.maproute.aspx) 擴充方法是 ASP.NET MVC (System.Web.Mvc) 的一部份。
6.  定義 **Route 名稱**，這個名稱雖然少用，但是開發到較為進階的 Routing 技巧時會用到。
7.  定義 **網址格式** 與每個 **網址段落**(**Path Segment**) 的 **RouteValue 參數名稱**。  
    注意: 該網址不能以斜線 ( / ) 開頭。
8.  定義**各 RouteValue 參數**的**預設值**

**基本觀念**

-   Routing (System.Web.Routing) 是 ASP.NET 3.5 SP1 的一部份，ASP.NET MVC 只是使用而已
-   [RouteTable](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.aspx).[Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 是一個**公開**的**靜態物件**，在整個 HttpApplication 生命週期裡都會固定存在!
-   由於 [RouteTable](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.aspx).[Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 是一個**公開**的**靜態物件**，所以在不用重新編譯網站的情況下是有可能動態調整 [RouteTable](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.aspx).[Routes](http://msdn.microsoft.com/zh-tw/library/system.web.routing.routetable.routes.aspx) 內容的。詳見：[Editable Routes](http://haacked.com/archive/2010/01/17/editable-routes.aspx) 與 [Editable Routes Using App\_Code](https://haacked.com/archive/2010/01/17/editable-routes-in-medium-trust.aspx/)
-   [IgnoreRoute](http://msdn.microsoft.com/en-us/library/dd470170.aspx) 與 [MapRoute](http://msdn.microsoft.com/en-us/library/system.web.mvc.routecollectionextensions.maproute.aspx) 這兩個擴充方法 (ExtensionMethod) 是 System.Web.Mvc 命名空間中所新增的 [RouteCollectionExtensions 類別](http://msdn.microsoft.com/en-us/library/system.web.mvc.routecollectionextensions.aspx)所提供的擴充方法，為了方便撰寫 Routing 規則之用。
-   在 Routing 規則集之中，第一個被比對成功的規則就會直接被採用，此原則跟 Firewall Rules 類似

**路由開發技巧**

**技巧 1：替 Routing 網址設立條件限制**

由於 ASP.NET MVC 有個很重要的特性：以習慣取代設定 ([Convention over Configuration](http://en.wikipedia.org/wiki/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](http://msdn.microsoft.com/en-us/library/system.web.mvc.routecollectionextensions.maproute.aspx) 參數，其中 \\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](http://msdn.microsoft.com/zh-tw/library/system.web.httprequest.islocal.aspx) 判斷來源電腦是否來自於本機，這樣我們可以寫出以下程式碼：

```
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](http://msdn.microsoft.com/zh-tw/library/system.web.httpapplication.postresolverequestcache\(v=VS.90\).aspx)）：

[![image](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/161c81d13268602fa6ed-image_985.png "image")](http://learn.iis.net/page.aspx/496/iis-url-rewriting-and-aspnet-routing/)

以下一些參考連結可以幫助你學習如何讓 ASP.NET Web Form 的網頁也套用 Routing 規則：

-   [Using Routing With WebForms](http://haacked.com/archive/2008/03/11/using-routing-with-webforms.aspx)
-   [URL Routing with ASP.NET 4 Web Forms (VS 2010 and .NET 4.0 Series)](https://weblogs.asp.net/scottgu/url-routing-with-asp-net-4-web-forms-vs-2010-and-net-4-0-series)
-   [Extreme ASP.NET - Routing with ASP.NET Web Forms](http://msdn.microsoft.com/en-us/magazine/dd347546.aspx)

**技巧 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;
```

如下圖示：

![](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/c0b88785dbf70a982cb4-image_986.png)

這樣的設計當然也會帶來一些副作用，因為當你真的有實體檔案要能直接透過 IIS 回應的時候，就必須針對特定的檔案類型或路徑設定 IgnoreRoute 或 StopRoutingHandler (本文**技巧 3** 有提到)。

**相關連結**

-   [MVC - Routing to a static file](https://web.archive.org/web/20111013044332/http://software.herbrandson.com:80/index.php/2009/06/mvc-routing-to-a-static-file/)
-   [How ASP.NET MVC WORKS?](http://mvcsharp.wordpress.com/2010/01/03/how-asp-net-mvc-works/)
-   [Routing with ASP.NET Web Forms](http://msdn.microsoft.com/en-us/magazine/dd347546.aspx)
-   [Creating a Custom Route Constraint](http://www.asp.net/mvc/tutorials/creating-a-custom-route-constraint-cs)
-   [RouteCollection.RouteExistingFiles](http://msdn.microsoft.com/en-us/library/system.web.routing.routecollection.routeexistingfiles.aspx)
-   [IIS URL Rewriting and ASP.NET Routing](https://learn.microsoft.com/en-us/iis/extensions/url-rewrite-module/iis-url-rewriting-and-aspnet-routing)
-   [ASP.NET MVC 開發心得分享 (8)：Routing 設定注意事項](/post/2009/07/20/ASPNET-MVC-Developer-Note-Part-8-Routing)
-   [ASP.NET MVC Tip #30 – Create Custom Route Constraints](https://learn.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/controllers-and-routing/creating-a-custom-route-constraint-cs)
-   [Getting ASP.NET Routing Up and Running - The Definitive Guide](https://learn.microsoft.com/en-us/archive/blogs/mikeormond/getting-asp-net-routing-up-and-running-the-definitive-guide)
