本文章是由機器翻譯。
技術最前線
ASP.NET MVC 中的動態動作篩選器
Dino Esposito
上個月,我討論了 ASP.NET MVC 應用程式中操作篩選器的角色和實現。我們回顧一下:操作篩選器是一些對控制器方法和類進行修飾的屬性,用於使控制器方法和類執行一些可選操作。例如,您可以編寫一個 Compress 屬性,讓它通過壓縮的 gzip 流透明篩選方法生成的任何回應。主要優點在於,壓縮代碼隔離在便於重用的單獨類中,這有助於盡可能減少方法的負擔。
不過,屬性是靜態的。若要充分發揮其內在的靈活性,還需要執行編譯步驟。更改控制器類的其他方面很容易,不過,需要修改原始程式碼才能實現。一般而言,這不是大問題。大部分代碼維護工作都需要對原始程式碼進行物理更改。在不會引入回歸風險情況下,越有效地進行更改越好。
對於內容和功能常常變化的網站(主要是網站門戶)和高度可自訂的軟體即服務 (SaaS) 應用程式,任何可以避免觸及原始程式碼的解決方案都更受歡迎。因此,問題是,如何動態載入操作篩選器?答案是當然有,本文其餘部分將對此進行說明。
深入瞭解 ASP.NET MVC
ASP.NET MVC 框架公開了大量介面和可重寫方法,通過這些介面和方法,幾乎可以對該框架的所有方面進行自訂。簡而言之,控制器方法的整個操作篩選器集合都載入並保存在駐留記憶體的清單中。作為開發人員,您可以訪問和檢查該清單。通過多做一些工作,您可以修改操作篩選器的清單,甚至可以動態填充該清單。
現在,我們概述一下框架為了執行操作所執行的步驟,深入瞭解相關工作原理。在這個過程中,將使用可實現動態篩選器的中心元件:操作調用程式。
操作調用程式最終負責控制器類所有操作方法的執行。操作調用程式實現每個 ASP.NET MVC 請求的內部生命週期。調用程式是實現 IActionInvoker 介面的類的實例。每個控制器類都有自己的調用程式物件,通過名為 ActionInvoker 的普通 get/set 屬性對外公開。該屬性是在基 System.Web.Mvc.Controller 類型上定義的,如下所示:
public IActionInvoker ActionInvoker {
get {
if (this._actionInvoker == null) {
this._actionInvoker = this.CreateActionInvoker();
}
return this._actionInvoker;
}
set {
this._actionInvoker = value;
}
}
CreateActionInvoker 方法是 Controller 類型的受保護可重寫方法。 以下是其實現:
protected virtual IActionInvoker CreateActionInvoker() {
// Creates an instance of the built-in invoker
return new ControllerActionInvoker();
}
事實上,可以針對任何控制器隨意更改操作調用程式。 不過,因為請求生命週期的早期階段就涉及調用程式,所以,可能需要控制器工廠將您自己的調用程式交換為預設調用程式。 通過這種方法,並結合控制反轉 (IoC) 框架(如 Unity),您可以直接從 IoC 容器的(離線)設置中更改調用程式邏輯。
作為替代方法,您也可以為自己的應用程式定義自訂控制器基類,並重寫 CreateActionInvoker 方法,使其只返回需要的調用程式物件。 ASP.NET MVC 框架正是使用這種方法來支援控制器操作的非同步執行。
操作調用程式是針對 IActionInvoker 介面構建的,這個介面非常簡單,因為它只公開一個方法:
public interface IActionInvoker {
bool InvokeAction(
ControllerContext controllerContext,
String actionName);
}
注意預設操作調用程式,我們回顧一下操作調用程式應完成的主要任務。 調用程式首先獲取請求之後有關控制器的資訊以及要執行的特定操作。 資訊可從特定描述符物件獲取。 描述符包含控制器的名稱和類型,以及屬性和操作的清單。 出於性能考慮,調用程式會構建自己的操作和控制器描述符緩存。
大致看一下圖 1 中 ControllerDescriptor 類的原型,您會發現很有趣。 這個類只表示任何實際描述符的基類。
圖 1 ControllerDescriptor 類
public abstract class ControllerDescriptor :
ICustomAttributeProvider {
// Properties
public virtual string ControllerName { get; }
public abstract Type ControllerType { get; }
// Method
public abstract ActionDescriptor[] GetCanonicalActions();
public virtual object[] GetCustomAttributes(bool inherit);
public abstract ActionDescriptor FindAction(
ControllerContext controllerContext,
string actionName);
public virtual object[] GetCustomAttributes(
Type attributeType, bool inherit);
public virtual bool IsDefined(
Type attributeType, bool inherit);
}
ASP.NET MVC 框架使用兩個在內部大量使用 Microsoft .NET Framework 反射的具體描述符類。 一個名為 ReflectedControllerDescriptor;另一個只用于非同步控制器,名為 ReflectedAsyncControllerDescriptor。
我很難想像出需要創建自己的描述符的情況。 但是,為了滿足大家的好奇心,我們看看是如何實現的。
假設您創建一個派生描述符類,並重寫 GetCanonicalActions 方法,以便從設定檔或資料庫表中讀取受支援操作的清單。 這樣,您可以根據一些配置內容從清單中刪除有效操作方法。 為此,您需要使用自己的操作調用程式,並相應編寫其 GetControllerDescriptor 方法,以返回自訂描述符的實例:
protected virtual ControllerDescriptor
GetControllerDescriptor(
ControllerContext controllerContext);
獲取有關控制器和操作方法的資訊,這只是操作調用程式完成的第一步。接下來,對於本文主旨而言,更為有趣的是,操作調用程式會為處理的方法獲取操作篩選器的清單。此外,操作調用程式還檢查使用者的授權許可權,針對可能危險的已發佈資料驗證請求,然後調用方法。
獲取操作篩選器的清單
即使操作調用程式用 IActionInvoker 介面來標識,ASP.NET MVC 框架也會使用內置類 ControllerActionInvoker 的服務。該類支援許多附加方法和功能,包括上述描述符和操作篩選器。
ControllerActionInvoker 類提供兩個用於處理操作篩選器的主幹預點。一個是 GetFilters 方法:
protected virtual ActionExecutedContext
InvokeActionMethodWithFilters(
ControllerContext controllerContext,
IList<IActionFilter> filters,
ActionDescriptor actionDescriptor,
IDictionary<string, object> parameters);
另一個是 InvokeActionMethodWithFilters 方法:
protected virtual FilterInfo GetFilters(
ControllerContext controllerContext,
ActionDescriptor actionDescriptor)
可以看到,這兩個都是受保護的虛方法。
調用程式在需要訪問為給定操作定義的篩選器清單時會調用 GetFilters。 您可能猜到了,這發生于請求生命週期的早期,早于對方法 InvokeActionMethodWithFilters 的所有調用。
您應該會注意到,在調用 GetFilters 之後,調用程式會保留整個篩選器清單,使其可供每個可能的類別使用,包括異常篩選器、結果篩選器、授權篩選器,當然還有操作篩選器。 考慮以下控制器類:
[HandleError]
public class HomeController : Controller {
public ActionResult About() {
return View();
}
}
整個類用 HandleError 屬性(該屬性為異常篩選器)來修飾,沒有其他屬性可見。
現在,我們添加一個自訂調用程式,重寫方法 GetFilters,並在代碼的最後一行放置中斷點,如下所示:
protected override FilterInfo GetFilters(
ControllerContext controllerContext,
ActionDescriptor actionDescriptor) {
var filters = base.GetFilters(
controllerContext, actionDescriptor);
return filters;
}
图 2 是變數篩選器的實際內容。
圖 2 截獲篩選器集合的內容
FilterInfo 類是 System.Web.Mvc 中的公共類,它為每個類別提供特定的篩選器集合:
public class FilterInfo {
public IList<IActionFilter> ActionFilters { get; }
public IList<IAuthorizationFilter> AuthorizationFilters { get; }
public IList<IExceptionFilter> ExceptionFilters { get; }
public IList<IResultFilter> ResultFilters { get; }
...
}
如圖 2 所示,對於前面介紹的簡單類,計數為一個操作篩選器、一個授權篩選器、一個結果篩選器和兩個異常篩選器。 誰定義了操作、結果和授權篩選器? 控制器類本身是一個操作篩選器。 實際上,基 Controller 類實現了所有相關的篩選器介面:
public abstract class Controller :
ControllerBase, IDisposable,
IActionFilter, IAuthorizationFilter,
IExceptionFilter, IResultFilter {
...
}
GetFilters 的基實現使用 .NET Framework 中的反射來反射控制器類中的屬性。 在您對 GetFilters 方法的實現中,可以添加任意多個篩選器,從任何位置都可以讀取這些篩選器。 您只需要一段代碼,如下所示:
protected override FilterInfo GetFilters(
ControllerContext controllerContext,
ActionDescriptor actionDescriptor) {
var filters = base.GetFilters(
controllerContext, actionDescriptor);
// Load additional filters
var extraFilters = ReadFiltersFromConfig();
filters.Add(extraFilters);
return filters;
}
通過這種最具靈活性的方法,您可以實現任何目標,可以添加任何篩選器類型。
調用操作
InvokeActionMethodWithFilters 是在執行操作方法的過程中調用的。在此情況下,該方法接收相關操作篩選器的清單。不過現在,您仍然可以添加額外的篩選器。图 3 是 InvokeActionMethodWithFilters 的示例實現,該實現可動態添加用於壓縮輸出的操作篩選器。图 3 中的代碼先檢查調用的方法是否為特定方法,然後產生實體並添加新篩選器。不用說也知道,您可以通過任何合適的方式確定要載入的篩選器,包括從設定檔、資料庫或其他任何位置讀取。重寫 InvokeActionMethodWithFilters 方法時,只需檢查正在執行的方法、附加其他操作篩選器並調用基方法,以使調用程式可以正常運行。若要檢索有關正在執行的方法的資訊,可以利用控制器上下文和操作描述符。
圖 3 在執行操作之前添加操作篩選器
protected override ActionExecutedContext
InvokeActionMethodWithFilters(
ControllerContext controllerContext,
IList<IActionFilter> filters,
ActionDescriptor actionDescriptor,
IDictionary<String, Object> parameters) {
if (
actionDescriptor.ControllerDescriptor.ControllerName == "Home"
&& actionDescriptor.ActionName == "About") {
var compressFilter = new CompressAttribute();
filters.Add(compressFilter);
}
return base.InvokeActionMethodWithFilters(
controllerContext,
filters, actionDescriptor, parameters);
}
因此,有兩種方法可以向控制器實例動態添加篩選器:重寫 GetFilters 和重寫 InvokeActionMethodWithFilters。但是,兩者有什麼區別?
操作生命週期
執行 GetFilters 或 InvokeActionMethodWithFilters 的過程大致相同。確實存在一些區別,但區別無關緊要。為了解兩者之間的區別,我們仔細研究一下預設操作調用程式在執行操作方法時執行的步驟。图 4 對生命週期進行了總結。
圖 4 操作方法的生命週期
在獲取描述符後,調用程式獲取篩選器的清單並進入授權階段。此時,調用程式處理已註冊的所有授權篩選器。如果授權失敗,任何操作結果的執行都完全忽略所有篩選器。
接下來,調用程式檢查請求是否需要驗證已發佈資料,然後繼續執行操作方法,並載入當前已註冊的所有篩選器。
最後,如果要動態添加任何授權篩選器,只有通過 GetFilters 方法添加才會正常運行。如果只是想添加操作篩選器、結果篩選器或異常篩選器,這兩種方法的結果是一樣的。
動態篩選器
動態載入篩選器是一項可選功能,主要適用于功能常變的應用程式。通過篩選器(尤其是操作篩選器),開發人員可以以聲明方式打開和關閉行為,從而在 ASP.NET MVC 控制器類中實現面向方面的功能。
在編寫控制器類的原始程式碼時,您可以選擇向類或方法級別添加操作屬性。從外部資料來源讀取有關操作篩選器的資訊時,可能並不清楚如何組織資訊以清晰表示篩選器和對應的方法。在資料庫方案中,可以創建一個表,表中以方法和控制器名稱為鍵。在配置方案中,可能需要編寫一個自訂配置節,只提供所需的資訊。在任何情況下,ASP.NET MVC 框架都非常靈活,您可以根據每個方法(甚至每個調用)來確定要應用的篩選器。
Dino Esposito 是 Microsoft Press 在 2010 年出版的《Programming ASP.NET MVC》一書的作者。Esposito 定居於義大利,經常在世界各地的業內活動中發表演講。您可訪問他的博客,網址為 weblogs.asp.net/despos。
衷心感謝以下技術專家對本文的審閱: Scott Hanselman