通過一個模擬程序讓你明白ASP.NET MVC是如何運行的
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回複
七、實例的配置和定義
如右圖所示,我們的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的名稱。也就是說如果通過瀏覽器訪問地址https://localhost/mvcapp/?controller=Home&action=Index 可以訪問到Index.html中的內容(注:我們並沒有將Index.html作為站點的默認頁麵)。
接下來我簡單的介紹一下是哪些組建促使這個簡單的ASP.NET Web應用能夠按照MVC的模式來執行。為了使你能夠在真正的ASP.NET MVC找到匹配的組件,我們采用了相同的接口和類型名稱。
我定義了如下一個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: }
通過上麵定義的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: }
通過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: }
在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: }
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>
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 14:34:21