本文章是由機器翻譯。

領先技術

ASP.NET MVC 中的操作篩選器

Dino Esposito

目前許多軟體架構師所面對的最大難題是如何設計並實現既能滿足所有初始版本需求又能滿足之後出現的所有其他需求的應用程式。自 1991 年 ISO/IEC 9126 檔初稿問世以來,可維護性已成為軟體設計的基本屬性之一。(該檔對軟體品質進行了正式說明,將其細分為一組特徵和子特徵,其中之一就是可維護性。訪問 iso.org 可以獲得該檔的 PDF 版本。)

對於每個軟體,能夠滿足客戶當前和未來的需要無疑並不是一種新的需求。然而如今許多 Web 應用程式需要的是可維護性的細微的短期表現形式。很多時候客戶並不需要增加新功能或採用不同的方法實現現有功能。他們只希望您對較小的功能項進行添加、替換、配置或刪除。一個典型的例子是擁有大量使用者的網站開展的特定廣告活動。網站的總體行為不需要改變,但必須在現有操作基礎上執行一些附加操作。此外,這些更改通常不是持久性的。這些更改必須在幾周後刪除再在幾個月後重新加入進來,並進行不同的配置等。您需要具備通過組合較小的功能項程式設計實現任何所需功能的能力;您需要在不對原始程式碼造成較大影響的情況下跟蹤各種依賴關係;而且您需要向面向方面的軟體設計邁進。這些都是控制反轉 (IoC) 框架之所以被許多企業專案迅速採用的主要原因。

那本文的內容是什麼呢?我們並不打算進行有關當今軟體變化方式的無聊演講,而是要深入探究 ASP.NET MVC 控制器的一項對於構建面向方面的 Web 解決方案有極大説明的強大功能:ASP.NET MVC 操作篩選器。

那麼,操作篩選器究竟是什麼呢?

操作篩選器是這樣一個屬性,將該屬性附加到控制器類或控制器方法時,可以提供將某種行為附加到所請求操作的聲明性方法。通過編寫操作篩選器,可以掛接某個操作方法的執行管道,並使其滿足您的需要。採用這種方法,您還可以從控制器類中取出嚴格來講並不屬於該控制器的任何邏輯。這樣做,可以使該特定行為成為可重用行為,更重要的是,使其成為可選行為。操作篩選器非常適用于實現影響控制器壽命的橫切關注點。

ASP.NET MVC 附帶了幾個預定義的操作篩選器,如 HandleError、Authorize 和 OutputCache。HandleError 用於捕獲對目標控制器類執行方法的過程中引發的異常。使用 HandleError 屬性的程式設計介面,您可以指定要與某個給定異常類型關聯的錯誤視圖。

Authorize 屬性用於阻止未經授權的使用者執行某個方法。但它並不區分這些使用者是尚未登錄,還是已登錄但缺少執行給定操作的足夠許可權。在此屬性的配置中,您可以指定執行給定操作所需的任何角色。

OutputCache 屬性用於按指定的持續時間和請求的參數清單對控制器方法的回應進行緩存。

一個操作篩選器類可以實現若干介面。图 1 顯示了介面的完整清單。

圖 1 操作篩選器的介面

篩選器介面 說明
IActionFilter 在執行控制器方法之前和之後調用此介面中的方法。
IAuthorizationFilter 在執行控制器方法之前調用此介面中的方法。
IExceptionFilter 在執行控制器方法的過程中引發異常時調用此介面中的方法。
IResultFilter 在處理操作結果之前和之後調用此介面中的方法。

通常情況下,您最關注的是 IActionFilter 和 IResultFilter。讓我們進一步瞭解一下這兩個介面。以下是 IActionFilter 的定義:

public interface IActionFilter
{
  void OnActionExecuted(ActionExecutedContext filterContext);
  void OnActionExecuting(ActionExecutingContext filterContext);
}

實現 OnActionExecuting 方法可以在執行控制器操作之前執行代碼;實現 OnActionExecuted 可以對方法已確定的控制器狀態進行後處理。 上下文物件可提供大量運行時資訊。 以下是 ActionExecutingContext 的簽名:

public class ActionExecutingContext : ControllerContext
{
  public ActionDescriptor ActionDescriptor { get; set; }
  public ActionResult Result { get; set; }
  public IDictionary<string, object> ActionParameters { get; set; }
}

尤其是操作描述符,它可以提供有關操作方法的資訊,如方法的名稱、控制器、參數、屬性和其他篩選器。 ActionExecutedContext 的簽名與前者只有一點區別,如下所示:

public class ActionExecutedContext : ControllerContext
{
  public ActionDescriptor ActionDescriptor { get; set; }
  public ActionResult Result { get; set; }
  public bool Canceled { get; set; }
  public Exception Exception { get; set; }
  public bool ExceptionHandled { get; set; }
}

除了對操作說明和操作結果的引用,此類還提供可能已發生的異常的相關資訊,並提供兩個需要引起注意的布林型標誌。 ExceptionHandled 標誌表示您的操作篩選器獲得了一次將某個已發生的異常標記為已處理的機會。 Canceled 標誌與 ActionExecutingContext 類的 Result 屬性有關。

請注意,ActionExecutingContext 類的 Result 屬性的唯一用途是將生成任何操作回應的負擔從控制器方法轉移到一系列已註冊的操作篩選器。 如果任何操作篩選器為 Result 屬性指定了值,則永遠不會調用控制器類上的目標方法。 這樣可以繞過目標方法,將生成回應的負擔完全轉移到操作篩選器。 但是,如果某個控制器方法註冊了多個操作篩選器,它們將共用操作結果。 如果一個篩選器設置了操作結果,鏈中的所有後續篩選器都將收到屬性 Canceled 設置為 true 的 ActionExecuteContext 物件。 無論是在操作已執行步驟中以程式設計方式設置 Canceled,還是在操作執行中步驟中設置 Result 屬性,都永遠不會運行目標方法。

編寫操作篩選器

如前所述,在編寫自訂篩選器方面,大多數情況下您會對以下兩種篩選器感興趣,即對操作結果進行預處理和後處理的篩選器,以及在執行常規控制器方法之前和之後運行的篩選器。 操作篩選器類通常並不自行實現介面,而是由 ActionFilterAttribute 派生而來:

public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
{
  public virtual void OnActionExecuted(ActionExecutedContext filterContext);
  public virtual void OnActionExecuting(ActionExecutingContext filterContext);
  public virtual void OnResultExecuted(ResultExecutedContext filterContext);
  public virtual void OnResultExecuting(ResultExecutingContext filterContext);
}

覆蓋 OnActionExecuted 可以在方法的執行中添加一些自訂代碼。 覆蓋 OnActionExecuting 是執行目標方法的前提條件。 最後,覆蓋 OnResultExecuting 和 OnResultExecuted 可以在控制方法回應生成的內部步驟周圍放置代碼。

图 2 顯示了一個操作篩選器示例,以程式設計方式為應用該篩選器的方法的回應添加壓縮。

圖 2 用於壓縮方法回應的操作篩選器示例

public class CompressAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(
    ActionExecutingContext filterContext)
  {
    // Analyze the list of acceptable encodings
    var preferred = GetPreferredEncoding(
      filterContext.HttpContext.Request);

    // Compress the response accordingly
    var response = filterContext.HttpContext.Response;
    response.AppendHeader("Content-encoding", preferred.ToString());

    if (preferredEncoding == CompressionScheme.Gzip)
    {
      response.Filter = new GZipStream(
        response.Filter, CompressionMode.Compress);
    }

    if (preferredEncoding == CompressionScheme.Deflate)
    {
      response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
    }
    return;
  }

  static CompressionScheme GetPreferredEncoding(HttpRequestBase request)
  {
    var acceptableEncoding = request.Headers["Accept-Encoding"];
    acceptableEncoding = SortEncodings(acceptableEncoding);

    // Get the preferred encoding format 
    if (acceptableEncoding.Contains("gzip"))
      return CompressionScheme.Gzip;
    if (acceptableEncoding.Contains("deflate"))
      return CompressionScheme.Deflate;

    return CompressionScheme.Identity;
  }

  static String SortEncodings(string header)
  {
    // Omitted for brevity
  }
}

在 ASP.NET 中,壓縮通常是通過註冊一個 HTTP 模組實現的,該模組用於截取所有請求並壓縮其回應。 另外,也可以在 IIS 級別啟用壓縮。 ASP.NET MVC 對上述兩種方式提供很好的支援,而且還提供第三種選擇:基於每個方法控制壓縮。. 採用這種方法,您可以控制特定 URL 的壓縮層級,而無需對 HTTP 模組進行編寫、註冊和維護。

圖 2 所示,操作篩選器覆蓋了 OnActionExecuting 方法。 起初這聽起來可能有點奇怪,因為您可能預計將壓縮作為在返回某些回應之前您需要考慮的橫切關注點。 壓縮通過固有的 HttpResponse 的 Filter 屬性實現。 由運行時環境產生的所有回應都通過 HttpResponse 物件返回用戶端流覽器。 隨後,通過 Filter 屬性安裝在預設輸出流上的任何自訂流都可以更改所發送的輸出。 因此,在 OnActionExecuting 方法執行過程中,您只需要在預設輸出流的基礎上設置附加流即可。

但對於 HTTP 壓縮,最困難的部分是仔細權衡流覽器的各種首選項。 流覽器通過 Accept-Encoding 標頭髮送其壓縮首選項。 該標頭的內容指明流覽器只能處理某些特定編碼(通常是 gzip 和 deflate)。 為獲得良好表現,操作篩選器必須嘗試準確判斷流覽器可以處理哪些編碼。 這是比較棘手的任務。 RFC 2616 中詳細說明了 Accept-Encoding 標頭的作用(請參見 w3.org/Protocols/rfc2616/rfc2616-sec14.html)。 簡而言之,Accept-Encoding 標頭的內容可以包含一個 q 參數,用於為可接受的值指定優先順序。 例如,假設以下所有字串都是某一編碼的可接受值,儘管 gzip 顯然是其中的首選編碼,但其實只有在第一個字串中它才是首選項:

gzip, deflate
gzip;q=.7,deflate
gzip;q=.5,deflate;q=.5,identity

壓縮篩選器應將這一點考慮在內,就像圖 2 中的篩選器那樣。 以上細節應當使您加強這樣的意識,即編寫操作篩選器時會對請求的處理造成干擾。 因此,您所做的任何操作都應與用戶端流覽器的預期一致。

應用操作篩選器

如前所述,操作篩選器就是一個屬性,它既可以應用於各個方法,也可以應用於整個父類。 它的設置方法如下:

[Compress]
public ActionResult List()
{
  // Some code here
  ...
}

如果屬性類包含某些公共屬性,您可以使用熟悉的屬性工作表示法以聲明方式為這些屬性賦值:

[Compress(Level=1)]
public ActionResult List()
{
  ...
}

图 3 顯示了 Firebug 所報告的壓縮回應內容。

圖 3 通過壓縮屬性獲得的壓縮回應

屬性只是配置方法的一種靜態方式。這意味著要應用進一步的更改,還需要第二個編譯步驟。不過,以屬性形式表示的操作篩選器提供了一個重要優勢:它們將橫切關注點保持在核心操作方法以外。

廣泛認識操作篩選器

為度量操作篩選器的真實作用,可以考慮隨著時間推移需要大量自訂工作的應用程式,以及為不同客戶安裝時需要進行改寫的應用程式。

例如,假設一個網站有時會開展基於獎勵分數的廣告活動,獎勵分數是註冊使用者通過在某個網站內執行標準操作(購買商品、回答問題、聊天、寫博客等)獲得的。作為開發人員,您可能需要在執行交易、發佈評論或開始聊天的常規方法運行之後運行的某種代碼。遺憾的是,這種代碼屬於往來代碼,通常不包括在核心操作方法的原始實現中。使用操作篩選器,您可以為每種必要方案創建不同的元件,還可以實現其他功能,例如,安排某個操作篩選器用於增加獎勵分數。接著,將獎勵篩選器附加到需要 post 操作的所有方法;然後重新編譯並運行。

[Reward(Points=100)]
public ActionResult Post(String post)
{
  // Core logic for posting to a blog
  ...
}

如前所述,屬性是靜態的,因此需要增加一個編譯步驟。儘管並非所有情況都必須增加此步驟(例如在具有高靈活性功能的網站),但總比沒有好得多。至少,您可以獲得在不對現有功能造成較大影響的情況下快速更新 Web 解決方案的功能,這對於保持對回歸錯誤的嚴格控制很有好處。

動態載入

本文展示了控制器操作方法環境中的操作篩選器。我展示的標準方法是將篩選器編寫為用於以靜態方式修飾操作方法的屬性。但是,有一個基本的問題:您是否可以動態載入操作篩選器?

ASP.NET MVC 框架是一(大)段編寫良好的代碼,因此公開了大量介面和可覆蓋方法,使用它們,您幾乎可以對該框架的所有方面進行自訂。幸運的是,即將推出的 Model-View-Controller (MVC) 3 將繼續增強這種趨勢。根據公共發展路線圖,團隊為 MVC 3 制定的目標之一是實現所有級別的依賴關係注入。因此,前面有關動態載入的問題的答案就在於 MVC 框架的依賴關係注入能力。一種可能的成功策略是自訂操作調用程式,以便在執行方法之前獲得對篩選器清單的訪問。由於篩選器清單看起來就像一個普通的讀/寫集合物件,因此要對其進行動態填充應該不會太困難。不過這非常適合作為新欄目的素材。

Dino Esposito 是 2010 年 Microsoft Press 出版的《Programming ASP.NET MVC》一書的作者,也是《Microsoft .NET:Architecting Applications for the Enterprise》一書(Microsoft Press,2008 年)的合著者。Esposito 定居於義大利,經常在世界各地的業內活動中發表演講。您可以訪問他的博客,位址為 weblogs.asp.net/despos

衷心感謝以下技術專家對本文的審閱:Scott Hanselman