我曾經在【ASP.NET MVC 開發心得分享 (21):Routing 觀念與技巧】這篇文章中分享過幾個路由開發技巧,其中在【技巧 1:替 Routing 網址設立條件限制】的部分有示範如何透過簡單的 RegEx 規則運算式 (正則表達式) 來限制路由變數的內容規則。不過,通常你在網路上能查到的這些 路由限制 (Route Constraints) 範例,大多使用「正向表列」的方式進行比對,這的確在大部分開發情境下都是這樣用的,但在特定比較少見的開發情境下,你或許需要「反向表列」的方式來限定路由參數的比對規則,尤其是在 ASP.NET MVC 與 ASP.NET HttpHandler 混合執行的情況下,更容易遇到這樣的問題。
執行環境描述:
假設你的 ASP.NET MVC 網站想要新增一個 HttpHandler 在網站裡,這邊我寫了一個範例程式如下:
- 組件名稱:RouteConstraintsSample
- 命名空間:RouteConstraintsSample.HttpHandlers
- 類別名稱:IndexB
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace RouteConstraintsSample.HttpHandlers
{
public class IndexB : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
context.Response.Write("Hello World");
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
這裡我假設因為專案需求的關係,該 HttpHandler 的網址必須呈現以下網址格式:
那麼,我的 web.config 必須這樣定義:
<configuration>
<system.web>
<httpHandlers>
<add path="IndexB"
verb="GET,HEAD,POST"
type="RouteConstraintsSample.HttpHandlers.IndexB, RouteConstraintsSample"/>
</httpHandlers>
</system.web>
<system.webServer>
<handlers>
<add name="IndexB"
path="IndexB"
verb="GET,HEAD,POST"
type="RouteConstraintsSample.HttpHandlers.IndexB, RouteConstraintsSample"/>
</handlers>
</system.webServer>
</configuration>
設定完成後,如果你直接執行該網站,並試圖開啟 /IndexB 這個 HttpHandler 處理常式,你將會得到以下錯誤畫面:
然而,會發生這個錯誤,最主要的原因還是來自於 ASP.NET MVC 的路由定義,我們先來看看在 ASP.NET MVC 預設專案範本的路由定義如下,你可以看到,預設第一個 網址段落 (Path Segment) 便是 {controller} 這個路由變數:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
所以當執行如上圖的 http://localhost:25102/IndexB 位址時,網址路徑出現的第一個 IndexB 自然會被辨識為 {controller} 路由參數,而我們的 ASP.NET MVC 控制器中,又沒有任何名為 IndexB 的控制器類別,所以就引發了一個 HTTP 404 的錯誤。 ( 註: 這個錯誤是 ASP.NET MVC 送出來的 )
解決方案:
要解決這種網址路由衝突的情況,最簡單的方式就是透過 路由限制 (Route Constraints) 的定義,來排除那些我們不想要比對到的路由,以本文範例來說,也就是 IndexB 這一個。
如果我們 ASP.NET MVC 專案中只有兩個控制器,分別是 AccountController 與 HomeController 這兩個類別,你可能會把路由定義改成以下這樣。而這也是本文稍早所說的以「正向表列」的方式進行比對:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { controller = "(Home)|(Account)" }
);
不過,在實務上,一個網站專案,不可能只有兩個控制器類別,而且網站會一直長大,需求會不斷新增,如果一直用「正向表列」的方式定義,也會讓專案的關注點不夠分離而造成一些網站維護上的困擾,因為你每次新增控制器都要手動在 App_Start\RouteConfig.cs 新增一個限制條件,出錯的機率也非常高,尤其是在網站交接的過程中,不容易把這些細節交代清楚。
但是,前陣子就有同事來問我,他們希望能夠用「反向表列」的方式定義路由限制,但怎樣都無法使用 RegEx 寫出來,最後雖然用自訂 Routing 條件限制的方式寫出來 (透過一個自訂類別並實做 IRouteConstraint 介面的方式),但總覺得不死心,問我用 RegEx 真的可以寫出「反向表列」的規則定義嗎?
正所謂江湖一點訣,看得懂 RegEx 規則運算式是一種能夠閱讀天書的能力,但寫得出來才能算是真正有實力,大家都應該好好練練。這話說得不誇張,我在十幾年前玩 Perl 的時候,曾經有一整年的時間經常接觸 RegEx 規則運算式,所以對語法瞭若指掌,當我被問及這個需求時,還真的也想了一些時間才寫出來,今天這篇文章寫得這麼長,其實就是為了分享這段特殊的「反向表列」寫法而已。
最後,如果我們只想針對 IndexB 這個名稱當成路由限制條件,其路由定義如下:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { controller = "(?!^IndexB$).*" }
);
我利用規則運算式中的 零寬度右不合樣 (Negative Lookahead) 語法,來定義出一個「反向表列」的規則,其中零寬度右不合樣的比對規則中,還要加上 ^ 與 $ 符號,才能真正做到 100% 的「反向比對」,否則定義出來的樣式會導致「部分比對」的情況發生。
( 註: 我有另外一篇類似的文章可供讀者參考: 使用 Regular Expression 驗證密碼 )
如果你想要「反向表列」兩個以上的名稱,其範例如下:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { controller = "(?!^IndexB$)(?!^MyHandler$).*" }
);
本文範例程式專案已上傳至 GitHub,各位可以多加利用 GitHub for Windows 工具下載原始碼回去測試:
相關連結