[Day09] 進入MVC原始碼世界 Route & RouteTable 原始碼解析

前言

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

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

此篇同步發布在筆者Blog [Day09] 進入MVC原始碼世界 Route & RouteTable 原始碼解析

介紹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優化的能力。
  • 安全性:如接指向文件相對路徑無疑跟大家說你伺服器資料夾的結構,如果被有心人士(hacker)知道就可旁敲側擊攻擊您的伺服器.

RouteTable.Routes

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

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

RouteConfig.RegisterRoutes(RouteTable.Routes);

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

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

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物件並加入集合中.

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集合中)

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

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預設參數
routes.MapPageRoute(
    "PhysicalFile",
    "GetFile/{Name}",
    "~/PhysicalFile.aspx", true,
    new RouteValueDictionary()
    {
        { "Name","PhysicalFile"}
    });

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

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

Hello PhysicalFile.aspx

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

在 Route中建立處理客製化HttpHandler

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


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加入全域路由集合中

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

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

小結:

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

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

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


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