繼上一篇文章之后,本文將介紹 Controller 和 Action 的一些較高級特性,包括 Controller Factory、Action Invoker 和異步 Controller 等內容。
本文目錄
開篇:示例准備
文章開始之前,我們先來了解一下一個請求的發出到Action方法處理后返回結果的流程,請試着理解下圖:
本文的重點是 controller factory 和 action invoker。顧名思義,controller factory 的作用是創建為請求提供服務的Controller實例;action invoker 的作用是尋找並調用Action方法。MVC框架為這兩者都提供了默認的實現,我們也可以對其進行自定義。
首先我們為本文要演示的示例做一些准備,把暫時想到的要用的 View、Controller 和 Action 都創建好。新建一個空的MVC應用程序,在Models文件夾中添加一個名為 Result 的Model,代碼如下:
namespace MvcApplication2.Models { public class Result { public string ControllerName { get; set; } public string ActionName { get; set; } } }
在 /Views/Shared 文件夾下添加一個名為 Result.cshtml 的視圖(不使用Layout),添加代碼如下:
... <body> <div>Controller: @Model.ControllerName</div> <div>Action: @Model.ActionName</div> </body>
本文的所有Action方法將都使用這同一個View,目的是顯示被執行的Controller名稱和Action名稱。
然后我們創建一個名為Product的Controller,代碼如下:
public class ProductController : Controller { public ViewResult Index() { return View("Result", new Result { ControllerName = "Product", ActionName = "Index" }); } public ViewResult List() { return View("Result", new Result { ControllerName = "Product", ActionName = "List" }); } }
繼續添加一個名為Customer的Controller,代碼如下:
public class CustomerController : Controller { public ViewResult Index() { return View("Result", new Result { ControllerName = "Customer", ActionName = "Index" }); } public ViewResult List() { return View("Result", new Result { ControllerName = "Customer", ActionName = "List" }); } }
准備工作做好了,開始進入正題吧。
自定義 Controller Factory
Controller Factory,顧名思義,它就是創建 controller 實例的地方。想更好的理解Controller Factory是如何工作的,最好的方法就是自己去實現一個自定義的。當然,在實際的項目中我們很少會去自己實現,一般使用內置的就足夠。自定義一個Controller Factory需要實現 IControllerFactory 接口,這個接口的定義如下:
using System.Web.Routing; using System.Web.SessionState; namespace System.Web.Mvc { public interface IControllerFactory { IController CreateController(RequestContext requestContext, string controllerName); SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); void ReleaseController(IController controller); } }
我們創建一個名為 Infrastructure 文件夾,在這個文件夾中創建一個名為 CustomControllerFactory 的類文件,在這個類中我們將簡單的實現 IControllerFactory 接口的每個方法,代碼如下:
using System; using System.Web.Mvc; using System.Web.Routing; using System.Web.SessionState; using MvcApplication2.Controllers; namespace MvcApplication2.Infrastructure { public class CustomControllerFactory : IControllerFactory { public IController CreateController(RequestContext requestContext, string controllerName) { Type targetType = null; switch (controllerName) { case "Product": targetType = typeof(ProductController); break; case "Customer": targetType = typeof(CustomerController); break; default: requestContext.RouteData.Values["controller"] = "Product"; targetType = typeof(ProductController); break; } return targetType == null ? null : (IController)DependencyResolver.Current.GetService(targetType); } public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) { return SessionStateBehavior.Default; } public void ReleaseController(IController controller) { IDisposable disposable = controller as IDisposable; if (disposable != null) { disposable.Dispose(); } } } }
先來分析一下這個類。
這里最重要的方法是 CreateController,當MVC框架需要一個 Controller 來處理請求時調用該方法。它有兩個參數,一個是 RequestContext 對象,通過它我們可以得到請求相關的信息;第二個參數是一個string類型的controller名稱,它的值來自於URL。這里我們只創建了兩個Controller,所以我們在 CreateController 方法中進行了硬編碼(寫死了controller的名稱),CreateController 方法的目的是創建Controller實例。
在自定義的Cotroller Factory中,我們可以任意改變系統默認的行為,比如switch語句中的default節點:
requestContext.RouteData.Values["controller"] = "Product";
它將路由的controller值改為Product,使得執行的cotroller並不是用戶所請求的controller。
在本系列的 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 文章中也講了一個用 Ninject 創建Controller Factory的例子,使用的是 ninjectKernel.Get(controllerType) 方法來創建Controller實例。這里我們使用 MVC 框架提供的 DependencyResolver 類來創建:
return targetType == null ? null : (IController)DependencyResolver.Current.GetService(targetType);
靜態的 DependencyResolver.Current 屬性返回一個 IDependencyResolver 接口的實現,這個實現中定義了 GetService 方法,它根據 System.Type 對象(targetType)參數自動為我們創建 targetType 實例,和使用Ninject類似。
最后來看看實現 IControllerFactory 接口的另外兩個方法。
GetControllerSessionBehavior 方法,告訴MVC框架是否保留Session數據,這點放在文章后面講。
ReleaseController 方法,當controller對象不再需要時被調用,這里我們判斷controller對象是否實現了IDisposable接口,實現了則調用 Dispose 方法來釋放資源。
CustomControllerFactory 類分析完了。和 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 講的示例一樣,要使用自定義的Controller Factory還需要在 Global.asax.cs 文件的 Application_Start 方法中對自定義的 CustomControllerFactory 類進注冊,如下:
protected void Application_Start() { ... ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory()); }
運行程序,應用程序根據路由設置的默認值顯示如下:
你可以定位到任意 /xxx/xxx 格式的URL來驗證我們自定的 Controller Factory 的工作。
使用內置的 Controller Factory
為了幫助理解Controller Factory是如何工作,我們通過實現IControllerFactory接口自定義了一個Controller Factory。在實際的項目中,我們一般不會這么做,大多數情況我們使用內置的Controller Factory,叫 DefaultControllerFactory。當它從路由系統接收到一個請求后,從路由實例中解析出 controller 的名稱,然后根據名稱找到 controller 類,這個類必須滿足下面幾個標准:
- 必須是public。
- 必須是具體的類(非抽象類)。
- 沒有泛型參數。
- 類的名稱必須以Controller結尾。
- 類必須(間接或直接)實現IController接口。
DefaultControllerFactory類維護了一個滿足以上標准的類的列表,這樣當每次接收到一個請求時不需要再去搜索一遍。當它找到了合適的 controller 類,則使用Controller Activator(一會介紹)來創建Controller 類的實例。它內部是通過 DependencyResolver 類進行依賴解析創建 controller 實例的,和使用Ninject是類似的原理。
你可以通過繼承 DefaultControllerFactory 類重寫其中默認的方法來自定義創建 controller 的過程,下面是三個可以被重寫的方法:
- GetControllerType,返回Type類型,為請求匹配對應的 controller 類,用上面定義的標准來篩選 controller 類也是在這里執行的。
- GetControllerInstance,返回是IController類型,作用是根據指定 controller 的類型創建類型的實例。
- CreateController 方法,返回是 IController 類型,它是 IControllerFactory 接口的 CreateController 方法的實現。默認情況下,它調用 GetControllerType 方法來決定哪個類需要被實例化,然后把controller 類型傳遞給GetControllerInstance。
重寫 GetControllerInstance 方法,可以實現對創建 controller 實例的過程進行控制,最常見的是進行依賴注入。
在本系列的 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 文章中的示例就是一個對 GetControllerInstance 方法進行重寫的完整示例,在這就不重復演示了。
現在我們知道 DefaultControllerFactory 通過 GetControllerType 方法拿到 controller 的類型后,它把類型傳遞給 GetControllerInstance 方法以獲取 controller 的實例。那么,GetControllerInstance 又是如何來獲取實例的呢?這就需要講到另外一個 controller 中的角色了,它就是下面講的:Controller Activator。
Controller 的激活
當 DefaultControllerFactory 類接收到一個 controller 實例的請求時,在 DefaultControllerFactory 類內部通過 GetControllerType 方法來獲得 controller 的類型,然后把這個類型傳遞給 GetControllerInstance 方法以獲得 controller 的實例。
所以在 GetControllerInstance 方法中就需要有某個東西來創建 controller 實例,這個創建的過程就是 controller 被激活的過程。
默認情況下 MVC 使用 DefaultControllerActivator 類來做 controller 的激活工作,它實現了 IControllerActivator 接口,該接口定義如下:
public interface IControllerActivator { IController Create(RequestContext requestContext, Type controllerType); }
該接口僅含有一個 Create 方法,RequestContext 對象參數用來獲取請求相關的信息,Type 類型參數指定了要被實例化的類型。DefaultControllerActivator 類中整個 controller 的激活過程就在它的 Create 方法里面。下面我們通過實現這個接口來自定義一個簡單的 Controller Activator:
public class CustomControllerActivator : IControllerActivator { public IController Create(RequestContext requestContext, Type controllerType) { if (controllerType == typeof(ProductController)) { controllerType = typeof(CustomerController); } return (IController)DependencyResolver.Current.GetService(controllerType); } }
這個 CustomControllerActivator 非常簡單,如果請求的是 ProductController 則我們給它創建 CustomerController 的實例。為了使用這個自定的 Activator,需要在 Global.asax 文件中的 Application_Start 方法中注冊 Controller Factory 時給 Factory 的構造函數傳遞我們的這個 Activator 的實例,如下:
protected void Application_Start() { ... ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new CustomControllerActivator())); }
運行程序,把URL定位到 /Product ,本來路由將指定到 Product controller, 然后 DefaultControllerFactory 類將請求 Activator 創建一個 ProductController 實例。但我們注冊了自義的 Controller Activator,在這個自定義的 Activator 創建 Controller 實例的的時候,我們做了一個“手腳”,改變了這種默認行為。當請求創建 ProductController 實例時,我們給它創建了CustomerController 的實例。結果如下:
其實更多的時候,我們自定義 controller 的激活機制是為了引入IoC,和 [ASP.NET MVC 小牛之路]05 - 使用 Ninject 講的通過繼承 DefaultControllerFactory 引入 IoC 是一個道理。
自定義 Action Invoker
當 Controller Factory 創建好了一個類的實例后,MVC框架則需要一種方式來調用這個實例的 action 方法。如果創建的 controller 是繼承 Controller 抽象類的,那么則是由 Action Invoker 來完成調用 action 方法的任務,MVC 默認使用的是 ControllerActionInvoker 類。如果是直接繼承 IController 接口的 controller,那么就需要手動來調用 action 方法,見上一篇 [ASP.NET MVC 小牛之路]09 - Controller 和 Action (1) 。下面我們通過自定義一個 Action Invoker 來了解一下 Action Invoker 的運行機制。
創建一個自定義的 Action Invoker 需要實現 IActionInvoker 接口,該接口的定義如下:
public interface IActionInvoker { bool InvokeAction(ControllerContext controllerContext, string actionName); }
這個接口只有一個 InvokeAction 方法。ControllerContext 對象參數包含了調用該方法的controller的信息,string類型的參數是要調用的Action方法的名稱,這個名稱來源於路由系統。返回值為bool類型,當actoin方法被找到並被調用時返回true,否則返回false。
下面是實現了IActionInvoker接口的 CustomActionInvoker 類:
using System.Web.Mvc; namespace MvcApplication2.Infrastructure { public class CustomActionInvoker : IActionInvoker { public bool InvokeAction(ControllerContext controllerContext, string actionName) { if (actionName == "Index") { controllerContext.HttpContext.Response.Write("This is output from the Index action"); return true; } else { return false; } } } }
這個 CustomActionInvoker 不需要關心實際被調用的Action方法。如果請求的是Index Action,這個 Invoker 通過 Response 直接輸出一個消息,如果不是請Index Action,則會引發一個404-未找到錯誤。
決定Controller使用哪個Action Invoker是由 Controller 中的 Controller.ActionInvoker 屬性來決定的,由它來告訴MVC當前的 controller 將使用哪個 Action Invoker 來調用 Action 方法。如下我們創建一個ActionInvokerController,並在它的構造函數中指定了 Action Invoker 為我們自定義的 Action Invoker:
namespace MvcApplication2.Controllers { public class ActionInvokerController : Controller { public ActionInvokerController() { this.ActionInvoker = new CustomActionInvoker(); } } }
這個 controller 中沒有 Action 方法,它依靠 CustomActionInvoker 來處理請求。運行程序,將URL定位到 /ActionInvoker/Index 可見如下結果:
如果將URL定位到 ActionInvoker 下的其他Action,則會返回一個404的錯誤頁面。
我們不推薦去實現自己的Action Invoker。首先內置的Action Invoker提供了一些非常有用的特性;其次是缺乏可擴展性和對View的支持等。這里只是為了演示和理解MVC框架處理請求過程的細節。
使用內置的 Action Invoker
通過自定義 Action Invoker,我們知道了MVC調用 Action 方法的機制。我們創建一個繼承自 Controller 抽象類的 controller,如果不指定Controller.ActionInvoker,那么MVC會使用內置默認的Action Invoker,它是 ControllerActionInvoker 類。它的工作是把請求匹配到對應的 Action 方法並調用之,簡單說就是尋找和調用 Action 方法。
為了讓內置的 Action Invoker 能匹配到 Action 方法,Action方法必須滿足下面的標准:
- 必須是公共的(public)。
- 不能是靜態的(static)。
- 不能是System.Web.Mvc.Controller中存在的方法,或其他基類中的方法。如方法不能是 ToString 和 GetHashCode 等。
- 不能是一個特殊的名稱。所謂特殊的名稱是方法名稱不能和構造函數、屬性或者事件等的名稱相同。
注意,Action方法也不能帶有泛型,如MyMethod<T>(),雖然 Action Invoker 能匹配到,但會拋出異常。
內置的 Action Invoker 給我們提供了很多實用的特性,給開發帶來很大便利,下面兩節內容可以說明這一點。
給 Action 方法定義別名
默認情況下,內置的Action Invoker (ControllerActionInvoker)尋找的是和請求的 action 名稱相同的 action 方法。比如路由系統提供的 action 值是 Index,那么 ControllerActionInvoker 將尋找一個名為 Index 的方法,如果找到了,它就用這個方法來處理請求。ControllerActionInvoker 允許我們對此行為進行調整,即可以通過使用 ActionName 特性對 action 使用別名,如下對 CustomerController 的 List action 方法使用 ActionName 特性:
public class CustomerController : Controller { ... [ActionName("Enumerate")] public ViewResult List() { return View("Result", new Result { ControllerName = "Customer", ActionName = "List" }); } }
當請求 Enumerate Action 時,將會使用 List 方法來處理請求。下面是請求 /Customer/Enumerate 的結果:
這時候對 /Customer/List 的請求則會無效,報“找不到資源”的錯誤,如下:
使用 Action 方法別名有兩個好處:一是可以使用非法的C#方法名來作為請求的 action 名,如 [ActionName("User-Registration")]。二是,如果你有兩個功能不同的方法,有相同的參數相同的名稱,但針對不同的HTTP請求(一個使用 [HttpGet],另一個使用 [HttpPost]),你可以給這兩個方法不同的方法名,然后使用 [ActionName] 來指定相同的 action 請求名稱。
Action 方法選擇器
我們經常會在 controller 中對多個 action 方法使用同一個方法名。在這種情況下,我們就需要告訴 MVC 怎樣在相同的方法名中選擇正確的 action 方法來處理請求。這個機制稱為 Action 方法選擇,它在基於識別方法名稱的基礎上,允許通過請求的類型來選擇 action 方法。MVC 框架可使用C#特性來做到這一點,所以這種作用的特性可以稱為 Action 方法選擇器。
內置 Action 方法選擇器
MVC提供了幾種內置的特性來支持 Action 方法選擇,它包括HttpGet、HttpPost、HttpPut 和 NonAction 等。這些選擇器從名字上很容易理解什么意思,這里就不解釋了。下面舉個 NonAction 的例子。在 CustomerController 中添加一個 MyAction 方法,然后應用 [NonAction] 特性,如下:
public class CustomerController : Controller { ... [NonAction] public ActionResult MyAction() { return View(); } }
使用 [NonAction] 后,方法將不會被識別為 action 方法,如下是請求 /Customer/MyAction 的結果:
當然我們也可以通過把方法聲明為 private 來告訴MVC它不是一個 action 方法。
自定義 Action 方法選擇器
除了使用內置的Action方法選擇器外,我們也可以自定義。所有的 action 選擇器都繼承自 ActionMethodSelectorAttribute 類,這個類的定義如下:
using System.Reflection; namespace System.Web.Mvc { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public abstract class ActionMethodSelectorAttribute : Attribute { public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo); } }
它是一個抽象類,只有一個抽象方法:IsValidForRequest。通過重寫這個方法,可以判斷某個請求是否允許調用 Action 方法。
我們來考慮這樣一種情況:同一個URL請求,在本地和遠程請求的是不同的 action (如對於本地則繞過權限驗證可能需要這么做)。那么自定義一個本地的 Action 選擇器會是一個不錯的選擇。下面我們來實現這樣一個功能的 Action 選擇器:
using System.Reflection; using System.Web.Mvc; namespace MvcApplication2.Infrastructure { public class LocalAttribute : ActionMethodSelectorAttribute { public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) { return controllerContext.HttpContext.Request.IsLocal; } } }
修改 CustomerController,添加一個LocalIndex 方法,並對它應用 “Index”別名,代碼如下:
public class CustomerController : Controller { public ViewResult Index() { return View("Result", new Result { ControllerName = "Customer", ActionName = "Index" }); } [ActionName("Index")] public ViewResult LocalIndex() { return View("Result", new Result { ControllerName = "Customer", ActionName = "LocalIndex" }); } ... }
這時如果請求 /Customer/Index,這兩個 action 方法都會被匹配到而引發歧義問題,程序將會報錯:
這時候我們再對 LocalIndex 應用我們自定義的 Local 選擇器:
... [Local] [ActionName("Index")] public ViewResult LocalIndex() { return View("Result", new Result { ControllerName = "Customer", ActionName = "Index" }); } ...
程序在本地運行的時候則會匹配到 LocalIndex action方法,結果如下:
通過這個例子我們也發現,定義了選擇器特性的Action方法被匹配的優先級要高於沒有定義選擇器特性的Action方法。
異步 Controller
對於 ASP.NET 的工作平台 IIS,它維護了一個.NET線程池用來處理客戶端請求。這個線程池稱為工作線程池(worker thread pool),其中的線程稱為工作線程(worker threads)。當接收到一個客戶端請求,一個工作線程從工作線程池中被喚醒並處理接收到的請求。當請求被處理完了后,工作線程又被這個線程池回收。這種線程程池的機制對ASP.NET應用程序有如下兩個好處:
- 通過線程的重復利用,避免了每次接收到一個新的請求就創建一個新的線程。
- 線程池維護的線程數是固定的,這樣線程不會被無限制地創建,減少了服務器崩潰的風險。
一個請求是對應一個工作線程,如果MVC中的action對請求處理的時間很短暫,那么工作線程很快就會被線程池收回以備重用。但如果執行action的工作線程需要調用其他服務(如調用遠程的服務,數據的導入導出),這個服務可能需要花很長時間來完成任務,那么這個工作線程將會一直等待下去,直到調用的服務返回才繼續工作。這個工作線程在等待的過程中什么也沒做,資源浪費了。設想一下,如果這樣的action一多,所有的工作線程都處於等待狀態,大家都沒事做,而新的請求來了又沒人理,這樣就陷入了尷尬境地。
解決這個問題需要使用異步(asynchronous) Controller,異步Controller允許工作線程在等待(await)的時候去處理別的請求,這樣做減少了資源浪費,有效提高了服務器的性能。
使用異步 Controller 需要用到.NET 4.5的新特性:異步方法。異步方法有兩個新的關鍵字:await 和 async。這個新知識點朋友們自己去網上找找資料看吧,這里就不講了,我們把重點放在MVC中的異步 Controller 上。
在Models文件夾中添加一個 RemoteService 類,代碼如下:
using System.Threading; using System.Threading.Tasks; namespace MvcApplication2.Models { public class RemoteService { public async Task<string> GetRemoteDataAsync() { return await Task<string>.Factory.StartNew(() => { Thread.Sleep(2000); return "Hello from the other side of the world"; }); } } }
然后創建一個名為 RemoteData 的 Controller,讓它繼承自 AsyncController 類,代碼如下:
using System.Web.Mvc; using MvcApplication2.Models; using System.Threading.Tasks; namespace MvcApplication2.Controllers { public class RemoteDataController : AsyncController { public async Task<ActionResult> Data() { string data = await new RemoteService().GetRemoteDataAsync(); Response.Write(data); return View("Result", new Result { ControllerName = "RemoteData", ActionName = "Data" }); } } }
運行程序,URL 定位到 /RemoteData/Data,2秒后將顯示如下結果:
當請求 /RemoteData/Data 時,Data 方法開始執行。當執行到下面代碼調用遠程服務時:
string data = await new RemoteService().GetRemoteDataAsync();
工作線程開始處於等待狀態,在等待過程中它可能被派去處理新的客戶端請求。當遠程服務返回結果了,工作線程再回來處理后面的代碼。這種異步機制避免了工作線程處於閑等狀態,盡可能的利用已被激活的線程資源,對提高MVC應用程序性能是很有幫助的。、
評論精選
提問 by 鹵鴿:
IActionInvoker是在Controller.Excute方法中被調用,主要是查詢具體的Action 處理方法,其實整個請求的過程都是從MvcHandler進行開始的。這是我的理解。不知正確否?
回答 by Liam Wang
是這么個意思,但不太嚴謹。確切一點說,Excute 方法是(自定義或默認的)ActionInvoker 的入口函數。 ActionInvoker 必須實現 IActionInvoker 接口來查找和調用 Action 方法。本文沒有介紹 MvcHandler 的知識。MvcHandler 是處理Controller的開始,但在MvcHandler 之前還有一個MvcRouteHandler,當請求經過路由解析后,MvcRouteHandler 實例會生成一個 MvcHandler 實例,並把請求交給它。MvcHandler 會從Controller 工廠中獲取一個 Controller 實例,然后由 Controller 來具體地處理請求。
PS:這篇文章寫到一半便在草稿箱中沉睡了一個多月,10月份20多天都在內蒙出差,回來都沒什么興致繼續寫下去。希望朋友們多多支持,讓我有動力把這個系列寫完。