天天看點

學習ASP.NET MVC5架構揭秘筆記-ASP.NET MVC是如何運作的(二)路由

路由

對于一個ASP.NET MVC應用來說,針對HTTP請求的處理實作在目标Controller類型的某個Action,每個HTTP請求不在像ASP.NET Web Forms應用一樣是針對一個實體檔案,而是針對某個Controller的某個Action方法。目标Controller和Action的名稱由HTTP請求的URL來決定,當ASP.NET MVC接收到抵達的請求後,其首要任務就是通過目前HTTP請求解析得到目标Controller和Action的名稱,這個過程是通過ASP.NET MVC的路由系統來實作的。我們通過如下幾個對象建構了一個簡易的路由系統。

1.RouteData

ASP.NET定義了一個全局的路由表,路由表中的每個Route對象包含一個路由模闆。目标Controller和Action的名稱可以通過路由變量以占位符的形式定義在模闆中,也可以作為路由對象的預設值(無須出現在路由模闆中)。對于每一個抵達的HTTP請求,路由系統會周遊路由表并找到一個具有與目前請求URL模式相比對的Route對象,然後利用它解析出以Controller和Action名稱為核心的路由資料。在我們自建的ASP.NET MVC架構中,通過路由解析得到的路由資料通過具有如下定義的RouteData類型表示。

public class RouteData
    {
        public IDictionary<string, object> Values { get; private set; }
        public IDictionary<string, object> DataTokens { get; private set; }
        public IRouteHandler RouteHandler { get; set; }
        public RouteBase Route { get; set; }
 
        public RouteData()
        {
            this.Values = new Dictionary<string, object>();
            this.DataTokens = new Dictionary<string, object>();
            this.DataTokens.Add("namespaces", new List<string>());
        }
 
        public string Controller
        {
            get
            {
                object controllerName = string.Empty;
                this.Values.TryGetValue("controller", out controllerName);
                return controllerName.ToString();
            }
        }
 
        public string ActionName
        {
            get
            {
                object actionName = string.Empty;
                this.Values.TryGetValue("action", out actionName);
                return actionName.ToString();
            }
        }
    }
           

RouteData定義了兩個字典類型的屬性Values和DataTokens,他們代表具有不同來源的路由變量,前者由對請求URL實施路由解析獲得。表示Controller和Action名稱的屬性直接從Values屬性的字典中提取,對應的Key分别為“controller”和“action”。

ASP.NET MVC的本質是由自定義的HttpModule和自定義的HttpHandler兩個元件來實作的。HttpModule從RouteData對象的RouteHandler屬性獲得。RouteData的RouteHandler屬性類型為IRouteHandler接口,該接口具有一個唯一的GetHttpHandler方法傳回真正真正處理HTTP請求的HttpHandler對象。該方法有一個類型為RequestContext的參數。RequestContext表示目前(HTTP)請求的上下文,其核心就是對目前HttpContext和RouteData的封裝。

public interface IRouteHandler
    {
        IHttpHandler GetHttpHandler(RequestContext requestContext);
    }
public class RequestContext
    {
        public virtual HttpContextBase HttpContext { get; set; }
        public virtual RouteData RouteData { get; set; }
    }
           

2.Route和RouteTable

承載路由變量的RouteData對象由路由表中與目前請求相比對的Route對象生成,可以通過RouteData的Route屬性獲得這個Route對象,該屬性的類型為RouteBase。如下面的代碼片段所示,RouteBase是一個抽象類,他僅僅包含一個傳回類型為RouteData的GetRouteData方法。

public abstract class RouteBase
    {
        public abstract RouteData GetRouteData(HttpContextBase httpContext);
    }
           

RouteBase的GetRouteData方法具有一個類型為HttpContextBase的參數,它代表針對目前請求的HTTP上下文。當該方法被執行的時候,它會判斷自身定義的路由規則是否與目前請求相比對,并在成功比對的情況下實施路由解析,并将得到的路由變量封裝成RouteData對象傳回。如果路由規則與目前請求不比對,則該方法直接傳回Null。

我們定義了如下一個繼承自RouteBase的Route類型來完成具體的路由工作。一個Route對象具有一個代表路由模闆的字元串類型的Url屬性。在實作的GetRouteData方法中,我們通過HttpContextBase擷取目前請求的URL,如果它與路由模闆的模式相比對,則建立一個RouteData對象作為傳回值。對于傳回的RouteData對象,其Values屬性表示的字典對對象包含直接通過解析出來的變量,而對于DataTokens字典和RouteHandler屬性,則直接取自Route對象的同名屬性。

public class Route : RouteBase
    {
        public IRouteHandler RouteHandler { get; set; }
        public string Url { get; set; }
        public IDictionary<string, object> DataTokens { get; set; }
 
        public Route()
        {
            this.DataTokens = new Dictionary<string, object>();
            this.RouteHandler = new MvcRouteHandler();
        }
 
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            IDictionary<string, object> variables;
            if (this.Match(httpContext.Request
                 .AppRelativeCurrentExecutionFilePath.Substring(2), out variables))
            {
                RouteData routeData = new RouteData();
                foreach (var item in variables)
                {
                    routeData.Values.Add(item.Key, item.Value);
                }
                foreach (var item in DataTokens)
                {
                    routeData.DataTokens.Add(item.Key, item.Value);
                }
                routeData.RouteHandler = this.RouteHandler;
                return routeData;
            }
            return null;
        }
 
        protected bool Match(string requestUrl,out IDictionary<string, object> variables)
        {
            variables = new Dictionary<string, object>();
            string[] strArray1 = requestUrl.Split('/');
            string[] strArray2 = this.Url.Split('/');
            if (strArray1.Length != strArray2.Length)
            {
                return false;
            }
            for (int i = 0; i < strArray2.Length; i++)
            {
                if (strArray2[i].StartsWith("{") && strArray2[i].EndsWith("}"))
                {
 
                    variables.Add(strArray2[i].Trim("{}".ToCharArray()), strArray1[i]);
                }
            }
            return true;
        }
    }
           

一個Web應用可以采用多種不同的URL模式,是以需要注冊多個繼承自RouteBase的Route對象,多個Route對象組成了一個路由表。在我們自定義的迷你版ASP.NET MVC架構中,路由表通過類型RouteTable表示。RouteTable僅僅具有一個類型為RouteDictionary的Routes屬性表示針對整個Web應用的全局路由表。

public class RouteTable
    {
        public static RouteDictionary Routes { get; private set; }
        static RouteTable()
        {
            Routes = new RouteDictionary();
        }
    }
           

RouteDictionary表示一個具名的Route對象的清單,我們直接讓它繼承自泛型的字典類型Dictionary<string,RouteBase>,其中的Key表示Route對象的注冊名稱。在GetRouteData方法中,我們周遊集合找到指定的HttpContextBase對象比對的Route對象,并得到對應的RouteData。

public class RouteDictionary : Dictionary<string, RouteBase>
    {
        public RouteData GetRouteData(HttpContextBase httpContext)
        {
            foreach (var route in this.Values)
            {
                RouteData routeData = route.GetRouteData(httpContext);
                if (null != routeData)
                {
                    return routeData;
                }
            }
            return null;
        }
    }
           

在Global.asax中我們建立了一個基于指定路由模闆的Route對象,并将其添加到通過RouteTable的靜态隻讀屬性Routes所表示的全局路由表中。

public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.Add("default", new Route { Url = "{controller}/{action}" });
        }
    }
           

3.UrlRoutingModule

路由表的作用是對目前的HTTP請求實施路由解析,進而得到一個以Controller和Action名稱為核心的路由資料,即上面介紹的RouteData對象。整個路由解析工作是通過一個類型為UrlRoutingModule的自定義IHttpModule來完成的。

public class UrlRoutingModule : IHttpModule
    {
        public void Dispose()
        { }
 
        public void Init(HttpApplication context)
        {
            context.PostResolveRequestCache += OnPostResolveRequestCache;
        }
        protected virtual void OnPostResolveRequestCache(object sender, EventArgs e)
        {
            HttpContextWrapper httpContext = new HttpContextWrapper(HttpContext.Current);
            RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);
            if (null == routeData)
            {
                return;
            }
            RequestContext requestContext = new RequestContext
            {
                RouteData = routeData,
                HttpContext = httpContext
            };
            IHttpHandler handler = routeData.RouteHandler.GetHttpHandler(requestContext);
            httpContext.RemapHandler(handler);
        }
    }
           

在實作的Init方法中,我們注冊了HttpApplication的PostResolveRequestCache事件。當代表目前應用的HttpApplication對象的PostResolveRequestCache事件觸發之後,UrlRoutingModule通過RouteTable的靜态隻讀屬性Routes得到表示全局路由表的RouteDictionary對象,然後根據目前HTTP上下文建立一個HttpContextWrapper對象(HttpContextWrapper是HttpContextBase的子類),并将其作為參數調用RouteDictionary對象的GetRouteData方法。

如果方法調用傳回一個具體的RouteData對象,UrlRoutingModule會根據該對象本身和之前得到的HttpContextWrapper對象建立一個表示目前上下文的RequestContext對象,并将其作為參數傳入RouteData的RouteHandler的GetHttpHandler方法得到一個HttpHandler對象。UrlRoutingModule最後調用HttpContextWrapper對象的RemapHandler方法對得到的HttpHandler對象進行映射,那麼針對目前HTTP請求的後續處理将由這個HttpHandler來接手。

下一章我們講解Controller的激活。