通過一個模擬程式讓你明白ASP.NET MVC是如何執行的

汪磊發表於2013-08-15

ASP.NET MVC的路由系統通過對HTTP請求的解析得到表示Controller、Action和其他相關的資料,並以此為依據啟用Controller物件,呼叫相應的Action方法,並將方法返回的ActionResult寫入HTTP回覆中。為了更好的演示其實現原理,我建立一個簡單的ASP.NET Web應用來模擬ASP.NET MVC的路由機制。這個例子中的相關元件基本上就是根據ASP.NET MVC的同名元件設計的,只是我將它們進行了最大限度的簡化,因為我們只需要用它來演示大致的實現原理而已。[原始碼從這裡下載]

目錄:
一、一個通過查詢字串表示Controller和Action的“MVC”程式
二、通過Route解析HTTP請求獲得路由資訊
三、在Global.asax中註冊Route
四、Route的執行
五、通過MvcHandler處理請求
六、將ActionResult寫入Http回覆
七、例項的配置和定義

一、一個通過查詢字串表示Controller和Action的“MVC”程式

image

如右圖所示,我們的Web應用非常簡單。HomeController.cs為定義Controller型別的檔案,而Index.html表示HomeController中名稱為Index的Action對應的View。我們按照ASP.NET MVC的原理,通過解析請求URL得到Controller和Action的名稱。如果Controller為Home,則啟用HomeController,如果當前的Action為Index,則將Index.html這個靜態檔案的內容作為HTTP回覆返回。

我不想定義複雜的解析Controller和Action的邏輯,再這裡我直接通過請求URL相應的查詢字串controler和action表示Controller和Action的名稱。也就是說如果通過瀏覽器訪問地址http://localhost/mvcapp/?controller=Home&action=Index 可以訪問到Index.html中的內容(注:我們並沒有將Index.html作為站點的預設頁面)。

image

接下來我簡單的介紹一下是哪些組建促使這個簡單的ASP.NET Web應用能夠按照MVC的模式來執行。為了使你能夠在真正的ASP.NET MVC找到匹配的元件,我們採用了相同的介面和型別名稱。

二、通過Route解析HTTP請求獲得路由資訊

我定義瞭如下一個RouteData型別表示解析HTTP請求得到的Controller和Action等資訊。Assemblies和Namespaces表示需要引入的名稱空間和程式集,這是因為URL中只能解析出Controller的型別名稱,需要相應的名稱空間採用得到它的型別全名。如果對應的程式集不曾載入,還需要載入相應的程式集。

   1: public class RouteData
   2: {
   3:     public string Controller { get; set; }
   4:     public string Action { get; set; }
   5:     public IList<string> Assemblies { get; private set; }
   6:     public IList<string> Namespaces { get; private set; }
   7:     public IRouteHandler RouteHandler { get; set; }
   8:  
   9:     public RouteData(string controller, string action, IRouteHandler routeHandler)
  10:     {
  11:         this.Controller = controller;
  12:         this.Action = action;
  13:         this.RouteHandler = routeHandler;
  14:         this.Namespaces = RouteTable.Namespaces;
  15:         this.Assemblies = RouteTable.Assemblies;
  16:     }
  17: }

真正實現對HTTP請求進行解析並得到RouteData的Route繼承自基類RouteBase。我們還定義個了一個表示Route集合的RouteCollection型別,它的GetRouteData方法對集合的所有Route物件進行遍歷,並呼叫其GetRouteData方法。如果得到的RouteData不為空,則返回之。

   1: public abstract class RouteBase
   2: {
   3:     public abstract RouteData GetRouteData(HttpContextBase httpContext);
   4: }
   5:  
   6: public class RouteCollection: Collection<RouteBase>
   7: {
   8:     public RouteData GetRouteData(HttpContextBase httpContext)
   9:     {
  10:         foreach (RouteBase route in this)
  11:         {
  12:             var routeData = route.GetRouteData(httpContext);
  13:             if (null != routeData)
  14:             {
  15:                 return routeData;
  16:             }
  17:         }
  18:         return null;
  19:     }
  20: }

和ASP.NET MVC一樣,我們定義瞭如下一個RouteTable物件,其靜態屬性正是一個RouteCollection物件。兩個靜態屬性Namespaces和Assemblies為名稱空間和程式集名稱的全域性維護。

   1: public class RouteTable
   2: {
   3:     static RouteTable()
   4:     {
   5:         Routes = new RouteCollection();
   6:         Namespaces = new List<string>();
   7:         Assemblies = new List<string>();
   8:     }
   9:     public static RouteCollection Routes { get; private set; }
  10:     public static IList<string> Namespaces { get; private set; }
  11:     public static IList<string> Assemblies { get; private set; }
  12: }

而我們例項中完成基於查詢字串的Controller和Action解析的QueryStringRoute對應如下。在GetRouteData方法中,除了根據查詢字元解析並初始化Controller和Action名稱之外,還將RouteHandler指定為MvcRouteHandler。而MvcRouteHandler得GetHttpHandler方法直接返回的是根據RequestContext建立的MvcHandler物件。

   1: public class QueryStringRoute : RouteBase
   2: {
   3:     public override RouteData GetRouteData(HttpContextBase httpContext)
   4:     {
   5:         if (httpContext.Request.QueryString.AllKeys.Contains("controller") &&
   6:             httpContext.Request.QueryString.AllKeys.Contains("controller") )
   7:         {
   8:             string controller = httpContext.Request.QueryString["controller"];
   9:             string action = httpContext.Request.QueryString["action"];
  10:             IRouteHandler routeHandler = new MvcRouteHandler();
  11:             return new RouteData(controller, action, routeHandler);               
  12:         }
  13:         return null;
  14:     }
  15: }
  16:  
  17: public class MvcRouteHandler: IRouteHandler
  18: {
  19:     public IHttpHandler GetHttpHandler(RequestContext requestContext)
  20:     {
  21:         return new MvcHandler(requestContext);
  22:     }
  23: }

三、在Global.asax中註冊Route

通過上面定義的RouteTable型別,我們在Global.asax中按照如下的方式在應用啟動的時候QueryStringRoute物件新增到RouteTable的靜態屬性Routes表示的Route列表中。同時為需要的名稱空間和程式集名稱進行初始化,以輔助後續步驟中對Controller的建立。

   1: public class Global : System.Web.HttpApplication
   2: {
   3:     protected void Application_Start(object sender, EventArgs e)
   4:     {
   5:         RouteTable.Routes.Add(new QueryStringRoute());
   6:         RouteTable.Assemblies.Add("MvcApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
   7:         RouteTable.Namespaces.Add("Artech.MvcApp");
   8:     }
   9: }

四、Route的執行

通過RouteTable的Routes屬性表示的Route列表對請求的解析和路由資訊的獲取是通過自定義的HttpModule來實現的,它的型別為UrlRoutingModule。如下面的程式碼片斷所示,UrlRoutingModule註冊了HttpApplication的PostResolveRequestCache事件,並在該事件觸發的時候呼叫Route列表的GetRouteData方法,並根據得到RouteData建立RequestContext。最後通過RouteData的RouteHandler得到真正用於處理該請求的HttpHandler物件,並對其進行對映。這意味著後續將會採用這個對映的HttpHandler進行請求的處理。

   1: public class UrlRoutingModule: IHttpModule
   2: {
   3:     public void Dispose() { }
   4:     public void Init(HttpApplication context)
   5:     {
   6:         context.PostResolveRequestCache += (sender, args) =>
   7:             {
   8:                 HttpContextWrapper contextWrapper = new HttpContextWrapper(context.Context);
   9:                 HttpContextBase httpContext = (HttpContextBase)contextWrapper;
  10:                 RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);
  11:                 if (null == routeData)
  12:                 {
  13:                     return;
  14:                 }
  15:                 RequestContext requestContext = new RequestContext { HttpContext = httpContext, RouteData = routeData };                    
  16:                 httpContext.RemapHandler(routeData.RouteHandler.GetHttpHandler(requestContext));
  17:             };
  18:     }
  19: }

五、通過MvcHandler處理請求

在UrlRoutingModule對映的實際上是具有如下定義的MvcHandler,它具有一個RequestContext屬性通過建構函式進行初始化。在ASP.NET MVC中,真正的請求處理體現在根據路由資訊建立Controller,並執行相應的Action方法。這兩個步驟體現的ProcessRequest方法中。

   1: public class MvcHandler: IHttpHandler
   2: {
   3:     public RequestContext RequestContext{get; private set;}
   4:     public IControllerFactory ControllerFactory
   5:     {
   6:         get { return ControllerBuilder.Current.GetControllerFactory(); }
   7:     }
   8:     public MvcHandler(RequestContext requestContext)
   9:     {
  10:         this.RequestContext = requestContext;
  11:     }
  12:     public bool IsReusable
  13:     {
  14:         get { return false; }
  15:     }
  16:     public void ProcessRequest(HttpContext context)
  17:     {
  18:         RouteData routeData = this.RequestContext.RouteData;
  19:         var controller =  this.ControllerFactory.CreateController(this.RequestContext, routeData.Controller);
  20:         controller.Execute(this.RequestContext);
  21:     }
  22: }

Controller實現了具有如下定義的介面IController,所有Action方法都通過Execute方法執行,該方法的引數的表示當前請求上下文的RequestContext物件。IController通過相應的Controller工廠建立,下面的程式碼同時也定義了Controller工廠介面的定義。

   1: public interface IController
   2: {
   3:     void Execute(RequestContext requestContext);
   4: }
   5: public interface IControllerFactory
   6: {
   7:     IController CreateController(RequestContext requestContext, string controllerName);
   8: }

我們定義瞭如下一個簡單名稱為DefaultController,它的Execute方法定義很簡單:通過包含在RequestContext的RouteData得到當前的Action,並將它作為方法名得到相應的MethodInfo物件,濱個通過反射呼叫它得到一個ActionResult物件,最後執行ActionResult的ExecuteResult方法。該方法的引數是基於RequestContext建立的另一個上下文ControllerContext。

   1: public class DefaultController : IController
   2: {
   3:     public void Execute(RequestContext requestContext)
   4:     {
   5:         string action = requestContext.RouteData.Action;
   6:         MethodInfo method = this.GetType().GetMethod(action);
   7:         ActionResult result = (ActionResult)method.Invoke(this, null);
   8:         ControllerContext controllerContext = new ControllerContext
   9:         {
  10:             RequestContext = requestContext
  11:         };
  12:         result.ExecuteResult(controllerContext);
  13:     }
  14: }

我們定義了具有如下定義的Controller工廠類DefaultControllerFactory。建立Controller的邏輯也不復雜:通過RouteData表示的Controller名稱得到相應的Controller型別,通過反射建立Controller物件。由於RouteData中只包含Controller的名稱,所以需要通過名稱空間和程式集的輔助才能解析出真正的型別。

   1: class DefaultControllerFactory : IControllerFactory
   2: {
   3:     public IController CreateController(RequestContext requestContext, string controllerName)
   4:     {
   5:         RouteData routeData = requestContext.RouteData;
   6:         string controllerType = string.Format("{0}Controller", controllerName);
   7:         IController controller;
   8:         controller = this.CreateControler(controllerType);
   9:         if (null != controller)
  10:         {
  11:             return controller;
  12:         }
  13:         foreach (string assembly in routeData.Assemblies)
  14:         {
  15:             controller = this.CreateControler(controllerType, assembly);
  16:             if (null != controller)
  17:             {
  18:                 return controller;
  19:             }
  20:  
  21:             foreach (string ns in routeData.Namespaces)
  22:             {
  23:                 controllerType = string.Format("{0}.{1}Controller", ns, controllerName);
  24:                 controller = this.CreateControler(controllerType, assembly);
  25:                 if (null != controller)
  26:                 {
  27:                     return controller;
  28:                 }
  29:             }
  30:         }
  31:  
  32:         throw new InvalidOperationException("Cannot locate the controller");
  33:     }
  34:     private IController CreateControler(string controllerType, string assembly = null)
  35:     {
  36:         Type type = null;
  37:         if (null == assembly)
  38:         {
  39:             type = Type.GetType(controllerType);
  40:         }
  41:         else
  42:         {
  43:             type = Assembly.Load(assembly).GetType(controllerType);
  44:         }
  45:         if (null == type)
  46:         {
  47:             return null;
  48:         }
  49:         return Activator.CreateInstance(type) as IController;
  50:     }
  51: }

六、將ActionResult寫入Http回覆

Controller的Action方法的返回值為具有如下定義的ActionResult型別,通過ExecuteResult方法將相應的執行結果寫入HTTP回覆中。我定義瞭如下一個StaticViewResult,它根據RouteData中的Action資訊找到匹配的.html靜態檔案,並將檔案的內容寫入HttpResponse。

   1: public abstract class ActionResult
   2: {
   3:     public abstract void ExecuteResult(ControllerContext context);
   4: }
   5:  
   6: public class StaticViewResult: ActionResult
   7: {
   8:     public override void ExecuteResult(ControllerContext context)
   9:     {
  10:         context.RequestContext.HttpContext.Response.WriteFile(context.RequestContext.RouteData.Action + ".html");
  11:     }
  12: }

七、例項的配置和定義

在我們的例項中定義的HomeController定義如下,在表示Action的Index方法中,直接返回一個StaticViewResult物件。

   1: public class HomeController : DefaultController
   2: {
   3:     public ActionResult Index()
   4:     {
   5:         return new StaticViewResult();
   6:     }
   7: }

然後在配置中進行了針對UrlRoutingModule的註冊,僅此而已。

   1: <configuration>
   2:   <system.webServer>
   3:     <modules>
   4:       <add name="UrlRoutingModule" type="Artech.MvcRouting.UrlRoutingModule, Artech.MvcRouting"/>
   5:     </modules>
   6:   </system.webServer>
   7: </configuration>

相關文章