精通ASP.NET MVC ——控制器可扩展性
MVC中引導動作方法執行過程的請求流程管道中,有兩個重要的部件:控制器工廠(Controller Factory) 和 動作調用器(Action Invoker)。控制器工廠負責創建對請求進行服務的控制器實例,動作調用器負責查找并調用控制器類中的動作方法。MVC框架中含有這兩個組件的默認實現,可以配置并控制他們的行為,也可以完全替換這些組件。
準備示例項目
新建一個空的MVC項目名叫ControllerExtensibility的項目。在Model中添加一個Result.cs文件,代碼如下圖所示:
namespace ControllerExtensibility.Models {public class Result{public string ControllerName { get; set; }public string ActionName { get; set; }} }在/Views/Shared文件夾下,添加一個名稱為Result.cshtml的視圖?,代碼如下:
@model ControllerExtensibility.Models.Result @{Layout = null; }<!DOCTYPE html><html> <head><meta name="viewport" content="width=device-width" /><title>Result</title> </head> <body><div>Controller:@Model.ControllerName</div><div>Action:@Model.ActionName</div> </body> </html>新增兩個控制器, Product控制器和Customer控制器,代碼如下圖所示:
public class CustomerController : Controller{// GET: Customerpublic ActionResult Index(){return View("Result",new Result { ControllerName = "Customer",ActionName = "Index" });}public ViewResult List(){return View("Result", new Result { ControllerName = "Customer",ActionName = "Index" });}} public class ProductController : Controller{// GET: Productpublic ActionResult Index(){return View("Result",new Result { ControllerName = "Product",ActionName = "Index" } );}public ViewResult List(){return View("Result", new Result { ControllerName = "Product", ActionName = "List" });}}這些控制器不執行任何有用的動作,只是通過Result.cshtml視圖報告他們已經被調用了。?
創建自定義控制器工廠?
控制器工廠是由IControllerFactory接口定義的,如下圖所示:
public interface IControllerFactory{IController CreateController(RequestContext requestContext, string controllerName);SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName);void ReleaseController(IController controller);}這個接口中最重要的方法是CreateController,當MVC框架需要控制器對請求進行服務時,便會調用這個方法。該方法的一個參數是一個RequestContext對象,它讓工廠能夠檢測請求的細節,另一個參數是一個字符串,它包含了從路由的URL那里得到所得到的controller值。
GetControllerSessionBehavior 方法由MVC框架用來確定是否應該為控制器維護會話數據。
當不在需要CreateController,方法創建的控制器對象時,會調用ReleaseController方法釋放資源。
下面簡單創建了一個控制器工廠,代碼如下:
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();}}}以上自定義控制器工廠只會指向?名叫Product 和 Customer 控制器,并且如果控制器不是這兩個,就默認指向Product控制器。
靜態的DependencyResolver.Current屬性返回IDependencyResolver接口的實現。該接口定義了GetService方法,為方法傳遞了一個System.Type對象。這里可以理解為實例化一個目標類型對象。
注冊使用自定義控制器工廠
通過ControllerBuilder類,可以告訴MVC框架使用這個自定義的控制器工廠。在Global.asax.cs文件中的Application_Start方法中加入如下代碼即可:
protected void Application_Start() {AreaRegistration.RegisterAllAreas();RouteConfig.RegisterRoutes(RouteTable.Routes);ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());//注冊 }一旦注冊了控制器工廠,將由它負責處理請求應用程序接收到的所有請求,啟動程序,就可以看到如下結果:?
? ? ? ? ? ??
?使用內建的控制器工廠
對于大多數程序應用程序,內建的控制器工廠類DefaultControllerFactory完全足夠滿足需求。當它從路由系統接收到一個請求時,該工廠考察路由數據,找到 Controller 屬性的值,并企圖在這個Web 應用程序中找到滿足如下條件的類:
1、這個類必須是一個public類。
2、這個類必須是具體類(不是抽象類)。
3、這個類必須沒有泛型參數。
4、這個類必須以Contoller結尾。
5、這個類必須實現IController接口。
DefaultControllerFactory類里有這些類的一個列表,一個請求到達時,它并不需要每次都執行一次搜索。如果找到了,便用控制器激活器(Controller Activator)創建一個實例。
命名空間優先排序
如果有同名控制器位于不同命名空間的,需要對命名空間優先排序,在Global.asax.cs文件中的Application_Start方法中加入如下代碼即可:
ControllerBuilder.Current.DefaultNamespaces.Add("MyControllerNamespace"); ControllerBuilder.Current.DefaultNamespaces.Add("MyProject.*");所有添加命名空間的順序,并不暗示搜索順序或者相對優先級——?所有Add方法定義的命名空間一視同仁。而優先級是相對于那些沒有Add的方法。如果控制器在Add方法中定義的命名空間找不到合適的控制器,那就會搜搜整個應用程序。
上述代碼中第二句的“ * ”表示的是查詢MyProject命名空間及所包含的子命名空間。
定制DefaultControllerFactory的控制器實例化
也可以通過創建一個控制器激活器(Controller Activator)對一個指定一個控制器類型進行實例化。代碼如下圖所示:
public class CustomerControllerActivator : IControllerActivator{public IController Create(RequestContext requestContext, Type controllerType){if (controllerType == typeof(ProductController)){controllerType = typeof(CustomerController);}return (IController)DependencyResolver.Current.GetService(controllerType);}}此IControllerActivator的實現很簡單——如果請求的是ProductController類,將以CustomerController類的實例作為其響應。?
為了激活這個自定義控制器,也需要在在Global.asax.cs文件中的Application_Start方法中加入如下代碼:
ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new CustomerControllerActivator()));啟動程序并導航到/Product,效果如下圖所示:?
? ? ? ? ? ??
?重寫DefaultControllerFactory方法
可以重寫DefaultControllerFactory類中的方法,來控制控制器的創建。
| 方法 | 結果 | 描述 |
| CreateController | IController | IControllerFactory接口的CreatController方法的實現。默認情況下,這個方法調用GetControllerType來確定應該實例化哪個類型,然后通過將結果傳遞給GetControllerInstance方法,來獲得一個控制器對象。 |
| GetControllerType | Type | 將請求映射到控制器類型。 |
| GetControllerInstance | IController | 創建指定類型的一個實例。 |
?創建自定義動作調用器
一旦控制器工廠創建了一個(控制器)類的實例,框架就需要一種辦法來調用這個實例上的一個動作。如果控制器是通過Controller類派生的,那么將由動作調用器(Action Invoker)調用動作。
動作調用器實現IActionInvoke接口,如下圖所示:
public interface IActionInvoker{bool InvokeAction(ControllerContext controllerContext, string actionName);}該接口只有一個單一的成員:InvokeAction(調用動作)?。其返回值是一個布爾類型的值,返回true,表示找到并調用了這個動作方法;false表示控制器沒有匹配的動作。
新增一個CustomActionInvoker.cs文件繼承此接口,代碼如下圖所示:
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;}}}這個動作方法并不關心控制器類中的方法。它只處理自己的動作。如果這是對Index動作的請求,那么該調用器直接將一條消息寫到Response。如果是對其他動作的請求,則返回false,這將會導致一個“404——未找到”的錯誤消息給用戶。
與一個控制器相關聯的動作調用器是通過Controller.ActionInvoker屬性獲得的,同一個應用程序中的不同控制器可以試用版不同的動作調用器。新增一個 ActionInvoker的新控制器,代碼如下:
public class ActionInvokerController : Controller{public ActionInvokerController() {this.ActionInvoker = new CustomActionInvoker();}}這個控制器中沒有動作方法,它依靠動作調用器去處理請求。通過啟動程序,并導航到/ActionInvoker/Index,可以看到其工作情況,而導航同一個控制器中的其他方法則看到404錯誤。?如下圖所示:
? ? ? ? ? ? ? ??
使用內建的動作調用器?
內建的動作調用器ControllerActionInvoker類,有一些將請求與動作方法進行匹配的非常完善的技術。默認的動作調用器是依靠方法進行操作的。為了具備一個動作的資格,一個方法必須滿足如下幾個條件:
1、該方法是必須是public的。
2、該方法必須不是staticd的。
3、該方法必須不是在 System.Web.Mvc.Controller或它的任何基類中。
4、該方法沒有專用名。
前兩個條件很簡單。第三個條件排除了Controller類或其基類出現的方法,這意味著不包括ToString及GetHashCode這樣的方法,因為這些都是IController接口實現的方法。最后一個條件意味著排除了構造器、屬性以及事件訪問器。
注:具有泛型參數的方法(如 MyMethod<T>() 滿足所有條件,但是如果視圖調用這樣的方法吹里一個請求,MVC框架會報錯)。
默認情況下,ControllerActionInvoker 查找一個具有與請求的動作同名的方法。?而且,MVC框架提供了一些可以調整的方法。
使用自定義動作名
通常,動作方法的名稱確定了它所表示的動作。Index動作方法對Index動作進行服務。但是可以用ActionName注解屬性來重寫這一行為。如下圖所示:
public class CustomerController : Controller{// GET: Customerpublic ActionResult Index(){return View("Result",new Result { ControllerName = "Customer",ActionName = "Index" });}[ActionName("Enumerate")]public ViewResult List(){return View("Result", new Result { ControllerName = "Customer",ActionName = "Index" });}}導航到/Customer/Enumerata,效果如下圖所示:?
? ? ? ? ? ? ??
這一注解屬性重寫了動作的名稱,這意味著導航到List方法不再工作,如下圖所示:
? ? ? ? ? ??
以這種方式重寫方法名的原因主要有兩個:
1、可以接收一個作為C# 方法名不合法的動作名,例如【ActionName(“User-Registration”】,其中“-”符號在C#中是不合法的。
2、如果希望有兩個不同的C#方法接受同一組參數,并且運用同樣的動作名(具有同樣參數的方法不能實現重載,只能采用不同的方法名),但是要對不同的HTTP請求類型進行響應,例如一個是【HttpGet】,而另一個是【HttpPost】那么可以對這些方法用不同的C#名來滿足編譯器的要求,然后用【ActionName】將他們映射到同一個動作名。
使用動作方法選擇?
很多情況是一個控制器中含有幾個同名的動作,這可能是因為有多個方法,每個方法的參數個數不同。或者是使用[ActionName]注解屬性,使多個方法表示同一個動作。
動作調用器在選擇一個動作時,會利用動作方法選擇器來消除不確定性。比如【HttPost】注解屬性就是一個動作方法的選擇器。首先會評估帶動作方法選擇器的動作,以考察其是否適合處理該請求。
【HttpGet】用于Get請求,【HttpPost】用于Post請求,另一個內建的注解屬性是NonAction(非動作),它向動作調用器解釋不應該作為作為動作方法來使用。如下圖所示:
[NonAction]public ActionResult MyAction(){return View();}上述代碼中的MyAction方法,將不會被看成是一個動作。以NonAction方法為目標的URL請求會生成“404——未找到”錯誤。另一個通常的方法是把這些方法標記為Private。?
創建自定義動作方法選擇器
動作方法選擇器派生于ActionMethodSelectorAttribute類,如下圖所示:
//// 摘要:// 表示一個特性,該特性用于影響操作方法的選擇。[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]public abstract class ActionMethodSelectorAttribute : Attribute{//// 摘要:// 初始化 System.Web.Mvc.ActionMethodSelectorAttribute 類的新實例。protected ActionMethodSelectorAttribute();//// 摘要:// 確定操作方法選擇對指定的控制器上下文是否有效。//// 參數:// controllerContext:// 控制器上下文。//// methodInfo:// 有關操作方法的信息。//// 返回結果:// 如果操作方法選擇對指定的控制器上下文有效,則為 true;否則為 false。public abstract bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo);}ActionMethodSelectorAttribute是一個抽象類,它定義了一個抽象方法:IsValidForRequest。該方法的一個參數是controllercontext對象,用來對請求進行檢測,另一個參數是MethodInfo對象,用來獲取運用了選擇器方法的信息。如果該方法能處理請求,便返回true,否則便返回false。
如下圖中創建了一個簡單的自定義選擇器,代碼如下圖所示:
public class LocalAttribute : ActionMethodSelectorAttribute{public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo){return controllerContext.HttpContext.Request.IsLocal;}}將該動作方法選擇器運運用,創建了Home控制器,代碼如下圖所示:?
public class HomeController : Controller{// GET: Homepublic ActionResult Index(){return View("Result",new Result { ControllerName = "Home",ActionName = "Index"});}[ActionName("Index")]public ActionResult LocalIndex(){return View("Result", new Result { ControllerName = "Home", ActionName = "LocalIndex" });}}上述代碼創建了兩個Index動作方法,因此,當/Home/Index請求達到時,動作調用器無法猜出應該使用哪一個,就會報錯:?
對Home控制器運用注解屬性:
public class HomeController : Controller{// GET: Homepublic ActionResult Index(){return View("Result",new Result { ControllerName = "Home",ActionName = "Index"});}[Local][ActionName("Index")]public ActionResult LocalIndex(){return View("Result", new Result { ControllerName = "Home", ActionName = "LocalIndex" });}}如果重啟程序,并從本地機器上運行瀏覽器導航到根URL,將會看到MVC框架已經考慮了方法的選擇注解屬性。解決了控制器類中方法之間的歧義問題:?
? ? ? ? ? ? ? ? ??
處理未知動作?
如果動作方法調用器找不到一個要調用的動作方法,便從它的InvokerAction方法返回false,當這種情況發生時,Controller類會調用它的HandleUnknowAction方法,默認情況下,這個方法會將一個“404——未找到”響應給客戶端。這是控制器大多數應用程序所能做的最有用的事情。如果想做一些特殊的事情,可以在控制器類中選擇重寫這個方法。代碼如下圖所示:
public class HomeController : Controller{// GET: Homepublic ActionResult Index(){return View("Result",new Result { ControllerName = "Home",ActionName = "Index"});}[Local][ActionName("Index")]public ActionResult LocalIndex(){return View("Result", new Result { ControllerName = "Home", ActionName = "LocalIndex" });}protected override void HandleUnknownAction(string actionName){Response.Write(string.Format("You requested the {0} action ",actionName));}}導航到一個不存在動作,如下圖所示:?
? ? ? ? ??
使用無會話控制器?
默認情況下,控制器是支持會話狀態的,這可以用來跨請求地存取數據值,使MVC程序員的工作更輕松。創建和維護會話狀態是一個棘手的過程。必須對數據進行存儲和接收,且必須對會話進行管理,以使他們能適當地終止。會話數據會消耗服務器內存或一些其他存儲單元空間。而且多個Web服務器之間的數據同步的需求,使得在服務器場(server farm)上運行應用程序更加困難。
為了簡化會話狀態,ASP.NET 對一個給定的會話在某一個時刻只處理一個查詢,如果客戶形成了多個重疊的請求,它們將被排成隊列,并由服務器依次處理。其好處是不需要擔心多個請求對同一數據進行修改的情況,缺點是得不到所希望的請求的吞吐量。
并非所有控制器都需要這種會話狀態特性。在這種情況下,能夠改善應用程序的性能,而又避免了棘手的會話狀態維護工作。這可以通過無會話控制器來實現。它們與規則控制器一樣,但是有兩個方面不同:在把它們用于處理一個請求時,MVC框架不加載或不存儲會話狀態,重疊請求可以同時處理。
IControllerFactory接口中,含有一個叫做“SessionStateBehavior”的方法,該方法返回SessionStateBehavior枚舉中的一個值。如下圖所示:
| 值 | SessionStateBehavior枚舉的值 |
| Default | 使用默認的ASP.NET行為,它會根據HttpContext來決定會話狀態的配置 |
| Required | 啟用完全會話狀態 |
| ReadOnly | 啟用只讀會話狀態 |
| Disable | 完全禁用會話狀態 |
通過返回GetControllerSessionBehavior方法的SessionStateBehavior的值,實現IControllerFactory接口的控制器工廠會直接設置控制器會話狀態的行為。?傳遞給這個方法的參數是RequestContext 和一個含有控制器名稱的字符串,可以返回如上圖中任意一個值,也可以根據不同的控制器返回不同的值,如下圖所示:
public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) {switch (controllerName){case "Home":return SessionStateBehavior.ReadOnly;case "Prouduct":return SessionStateBehavior.Required;default:return SessionStateBehavior.Default;} }?用DefaultControllerFactory管理會話狀態
當使用內建的控制器工廠(MVC 應用程序默認使用的就是這個默認的控制器工廠DefaultControllerFactroy)時,可以將SessionState注解屬性運用于每個控制器類,以便對控制器的會話進行控制,如下圖所示:
[SessionState(System.Web.SessionState.SessionStateBehavior.Disabled)] public class FastController : Controller {// GET: Fastpublic ActionResult Index(){return View("Result", new Result { ControllerName = "Fast",ActionName = "Index"});} }上述控制器運用了SessionState注解屬性,它影響著該控制器的所有動作。?Disable完全禁用了會話狀態,如果在控制中設置了一個會話值:
Session[" Message "]? = " Hello "
如果想從其他地方試圖讀取這個值,@Session[" Message "] ,MVC框架會報錯。HttpContext.Session屬性會返回Null。
如果制定了Readonly,那么可以讀取從其他控制器設置的值,但是企圖修改,也會報錯。
使用異步控制器?
核心ASP.NET 平臺維護著一個用來處理客戶端請求的.NET 線程池。這個線程池叫做“工作線程池(Work Thread Pool)”,而這些線程叫做“工作線程(Work Thread)”。當接受到一個請求時,將占用線程池中的一個工作線程,以進行這個請求的處理工作。當請求處理完成后,該工作線程被返回給線程池,以便用于新請求的處。對ASP.NET應用程序使用線程池有兩個好處:
1、通過重用工作線程,避免了每次處理一個請求時,都要創建一個新的線程的開銷(創建線程是需要時間的,若采用現有的線程就不一樣了)。
2、通過具有固定數目的可用工作線程,避免了超出服務器處理能力的并發請求情況。
在請求可以被短時間處理完畢的情況下,工作線程池工作的最好。這也是大多數MVC應用程序的情況。但是,如果有一些依賴于其他的服務器且占用較長時間才能完成的動作,那么你可能會遇到所有工作線程都被綁定于等待其他系統完成其工作的情況。
此刻服務器有能力做更多的工作——畢竟,這只是在等待,只占用了很少的資源——但是因為所有線程都被綁定,傳入的請求都被排成隊列。這將陷入應用程序處理停頓,而服務器大片的閑置的奇怪狀態。
這一問題的解決方案是使用異步控制器,這是提高應用程序的整體性能,但是不利于執行異步操作(即可提高性能,但實現(異步操作)難)。
注意:編寫并發代碼容易,編寫能夠正常工作的并發代碼是及其困難的。最好使用默認的線程池。特別是對于新手。即便是老手,也應該知道,編寫和測試一個新的線程池所付出的努力,與得到的回報是相比,是微不足道的。
異步控制器只能對占用I / O 或占用網絡帶寬,而且非CPU密集型的動作是有用的(CPU密集型動作是指,需要CPU高負荷運轉,占用較多內存,執行大量處理才能完成的動作)。??異步控制器解決的問題應當是,線程池與所處理的請求類型之間搭配不當的狀態。線程池意在確保每個請求到得到一片服務器資源,但是很可能停滯于一組無所事事的工作線程上。如果對CPU密集型動作使用額外的后臺線程,那么會因為涉及太多的并發請求而削弱服務器資源。
創建一個RemoteData常規同步控制器,如下圖所示:?
public class RemoteDataController : Controller{// GET: RemoteDatapublic ActionResult Index(){return View();}public ActionResult Data(){RemoteService service = new RemoteService();string data = service.GetRemoteData();return View((object)data);}}? ?RemoteService 實例代碼如下圖所示:
public class RemoteService{public string GetRemoteData(){Thread.Sleep(2000);return "Hello from the other side of world";}}添加對動作Data的新視圖,如下圖所示?:
@model string @{Layout = null; }<!DOCTYPE html><html> <head><meta name="viewport" content="width=device-width" /><title>Data</title> </head> <body><div> Data:@Model</div> </body> </html>運行效果如下圖所示:?
? ? ? ? ? ? ??
創建異步控制器?
使用關鍵字await 和 async,創建一個新的Task對象,并await它的響應。修改Data動作器代碼如下圖所示:
public async Task<ActionResult> Data() {RemoteService service = new RemoteService();string data = await Task<string>.Factory.StartNew( () => { return new RemoteService().GetRemoteData(); });return View((object)data); }在控制器中使用異步方法?
也可以在其他地方通過異步控制器來使用異步方法,在RemoteService.cs中添加如下方法:
public class RemoteService {public string GetRemoteData(){Thread.Sleep(2000);return "Hello from the other side of world";}public async Task<string> GetRemoteDataAsync(){return await Task<string>.Factory.StartNew(() => { Thread.Sleep(2000); return "Hello from the other side of the world"; });} }在控制器中調用異步方法:?
public class RemoteDataController : Controller {// GET: RemoteDatapublic ActionResult Index(){return View();}public async Task<ActionResult> Data(){RemoteService service = new RemoteService();string data = await Task<string>.Factory.StartNew( () => { return new RemoteService().GetRemoteData(); });return View((object)data);}public async Task<ActionResult> ConsumeAsyncMethod(){string data = await new RemoteService().GetRemoteDataAsync();return View("Data", (object)data);} }?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
總結
以上是生活随笔為你收集整理的精通ASP.NET MVC ——控制器可扩展性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 蒙特卡洛法求圆周率 c语言,c++蒙特卡
- 下一篇: mysql5.7空间运算,深度解析MyS