[ASP.NET MVC 小牛之路]10 - Controller 和 Action (2)

Liam Wang發表於2013-11-11

繼上一篇文章之後,本文將介紹 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多天都在內蒙出差,回來都沒什麼興致繼續寫下去。希望朋友們多多支援,讓我有動力把這個系列寫完。

 

相關文章