進入MVC原始碼世界 Route & RouteTable 原始碼解析 (第9天)

Agenda

前言

現在開始進入Asp.net MVC原始碼世界,我們從路由開始切入一步一步進入MVC核心.

我有做一個可以針對於Asp.net MVC Debugger的專案,只要下中斷點就可輕易進入Asp.net MVC原始碼.

如下面動畫

介紹Route

每個HTTP請求MVC使用路由的目標是ControllerAction,不像ASP.NET Web Form處理物理文件(.aspx文件),要執行ControllerAction名稱包含在HTTP請求中,ASP.NET MVC需要通過解析HTTP請求得到正確的ControllerAction的名稱。

使用Route處理物理文件有以下幾個優勢:

  • 靈活性:請求URL是對物理文件路徑,意味著如果物理文件的路徑發生了改變(比如改變了文件的目錄結構或者文件名),原來該文件連結將變得無效。
  • 可讀性:在很多情況下,URL不僅僅需要能夠訪問正確的網絡資源,也需要具有很好的可讀性,最好的URL應該讓我們一眼就能看出針對它訪問的目標資源是什麼。請求地址與物理文件緊密綁定讓我們完全失去了定義高可讀性URL的機會。
  • SEO優化:對於網站開發來說,為了迎合搜索引擎檢索的規則,我們需要對URL進行有效的設計使之能易於被主流的引擎檢索收錄。如果URL完全與物理地址關聯,這失去了SEO優化的能力。
  • 安全性:如接指向文件相對路徑無疑跟大家說你伺服器資料夾的結構,如果被有心人士(黑客)知道就可旁敲側擊攻擊您的伺服器.

RouteTable.Routes

在Global.cs檔案中,有一個RouteTable.RoutesRouteCollection類型的集合物件

我們通過RouteTable靜態屬性Routes得到一個全域的路由表,路由註冊的核心價值在此集合上添加路由設定。

1
RouteConfig.RegisterRoutes(RouteTable.Routes);

RouteCollection他是繼承Collection<RouteBase>的集合物件,可以對此集合添加一個繼承RouteBase物件.

在Mvc一般是透過MapRoute擴展方法來添加路由

1
2
3
4
5
6
7
8
9
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}

MapRoute擴展方法

看一下MapRoute原始碼,這個方式是基於RouteCollection集合物件做的擴展方法,可看到最重要的部分是新增一個Route物件並加入集合中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
{
// 判斷...
Route route = new Route(url, new MvcRouteHandler())
{
Defaults = CreateRouteValueDictionaryUncached(defaults),
Constraints = CreateRouteValueDictionaryUncached(constraints),
DataTokens = new RouteValueDictionary()
};

ConstraintValidation.Validate(route);

if ((namespaces != null) && (namespaces.Length > 0))
{
route.DataTokens[RouteDataTokenKeys.Namespaces] = namespaces;
}
//加入註冊路由器
routes.Add(name, route);

return route;
}

Route物件

Route類別是繼承於RouteBase(這也就是為什麼可以把Route物件加入RouteCollection集合中)

下面我刪減一些此次不會介紹到的程式碼.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class Route : RouteBase
{
private const string HttpMethodParameterName = "httpMethod";
private string _url;
private ParsedRoute _parsedRoute;
/// <summary>
/// 使用指定的 URL 模式、預設參數值、條件約束、自訂值和處理常式類別,初始化 <see cref="T:System.Web.Routing.Route" /> 類別的新執行個體。
/// </summary>
/// <param name="url">路由的 URL 模式。</param>
/// <param name="defaults">URL 未包含所有參數時所要使用的值。</param>
/// <param name="constraints">指定 URL 參數之有效值的規則運算式。</param>
/// <param name="dataTokens">
/// 傳遞給路由處理常式的自訂值,但不會用來判斷路由是否符合特定 URL 模式。
/// 這些值會傳遞至路由處理常式,以用來處理要求。
/// </param>
/// <param name="routeHandler">處理路由要求的物件。</param>
public Route(
string url,
RouteValueDictionary defaults,
RouteValueDictionary constraints,
RouteValueDictionary dataTokens,
IRouteHandler routeHandler)
{
this.Url = url;
this.Defaults = defaults;
this.Constraints = constraints;
this.DataTokens = dataTokens;
this.RouteHandler = routeHandler;
}

/// <summary>取得或設定運算式的字典,這些運算式指定 URL 參數的有效值。</summary>
public RouteValueDictionary Constraints { get; set; }

/// <summary>取得或設定自訂值,這些自訂值會傳遞給路由處理常式,但不會用來判斷路由是否符合 URL 模式。</summary>
public RouteValueDictionary DataTokens { get; set; }

/// <summary>取得或設定 URL 未包含所有參數時所要使用的值。</summary>
public RouteValueDictionary Defaults { get; set; }

/// <summary>取得或設定處理路由要求的物件。</summary>
public IRouteHandler RouteHandler { get; set; }


/// <summary>取得或設定路由的 URL 模式。</summary>
public string Url
{
get
{
return this._url ?? string.Empty;
}
set
{
this._parsedRoute = RouteParser.Parse(value);
this._url = value;
}
}

/// <summary>傳回所要求路由的相關資訊。</summary>
/// <param name="httpContext">封裝 HTTP 要求相關資訊的物件。</param>
/// <returns>包含路由定義值的物件。</returns>
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteValueDictionary values = this._parsedRoute.Match(httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo, this.Defaults);
if (values == null)
return (RouteData) null;
RouteData routeData = new RouteData((RouteBase) this, this.RouteHandler);
if (!this.ProcessConstraints(httpContext, values, RouteDirection.IncomingRequest))
return (RouteData) null;
foreach (KeyValuePair<string, object> keyValuePair in values)
routeData.Values.Add(keyValuePair.Key, keyValuePair.Value);
if (this.DataTokens != null)
{
foreach (KeyValuePair<string, object> dataToken in this.DataTokens)
routeData.DataTokens[dataToken.Key] = dataToken.Value;
}
return routeData;
}
}

Route類別中GetRouteData是個重要方法,藉由我們的路由設定去解析當前是否匹配到路由規則,如果有就回傳一個RouteData物件,否則回傳Null

上一篇有介紹UrlRoutingModule這個HttpModule會藉由RouteCollection.GetRouteData(context)動作取得一個RouteData並透過他拿到IHttpHander物件並給值到HttpContext.Handler

在裡面的實做是透過一個foreach去找尋匹配的Route物件,因為ADD路由是有順序性,所以在RegisterRoutes(RouteCollection routes)找尋路由會有第一個MapRoute到最後一個

Url這個屬性的set方法上做一個很有意思的動作,在設定值時除了賦值給_url字段,另外還將 設定template url Parse 取得一個ParsedRoute _parsedRoute物件.

  • ParsedRoute將我們注冊的template url用/分割存起來方便日後判斷執行的ActionContoller.

MapPageRoute 擴展方法

路由除了使用於取得調用ContollerAction資訊外,我們還可以通過MapPageRoute註冊URL樣板和某種文件的配對關係.

範例在:Asp.net MVC Debugger

本次使用幾個參數

  1. 路由名稱
  2. 樣版URL
  3. 指向實體aspx檔案路徑
  4. 此路由是否找尋實體路徑
  5. 樣版URL預設參數
1
2
3
4
5
6
7
8
routes.MapPageRoute(
"PhysicalFile",
"GetFile/{Name}",
"~/PhysicalFile.aspx", true,
new RouteValueDictionary()
{
{ "Name","PhysicalFile"}
});

下圖是我們專案建立一個新的.aspx檔案

裡面內容很簡單只是印出一段文字

1
Hello PhysicalFile.aspx

因為有加入MapPageRoute路由,在瀏覽器網址列輸入http:localhost:[your port]/GetFile,我們就可以將PhysicalFile.aspx檔案內容顯示出來.

在 Route中建立處理客製化HttpHandler

Route建構子中我們可以設定實現IRouteHandler物件,這個物件會有個方法可以返回IHttpHandlerasp.net請求使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

public class MyHandler : IHttpHandler
{
public bool IsReusable
{
get
{
return true;
}
}

public void ProcessRequest(HttpContext context)
{

context.Response.Write("Hello MyHandler!!");
}
}

public class MyHandlerRouter : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MyHandler();
}
}

我們可以建立MyHandlerRouterGetHttpHandler返回一個MyHandler物件,之後把MyHandlerRouter當作參數傳入Route物件中

Route加入全域路由集合中

1
routes.Add(new Route("Customer",new MyHandlerRouter()));

在瀏覽器輸入 http://localhost:[your port]/Customer 我們就會執行我們自己客製化的HttpHandler

小結:

路由封裝了Http請求路徑資訊可以讓我們找到相對應的ActionController並呼叫執行外,可以透過MapPageRoute來將請求教給.aspx實體檔案來處理請求.

Route甚至可以讓我們自己客製化處理HttpHandler在 Route中建立處理客製化HttpHandler可謂很有彈性

下篇介紹Route物件建立MvcRouteHandler物件如何取到IHttpHandler.

此文作者:Daniel Shih(石頭)
此文地址https://isdaniel.github.io/ithelp-day9/
版權聲明:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!


如果本文對您幫助很大,可街口支付斗內鼓勵石頭^^