【ASP.NET Core】MVC過濾器:執行流程

東邪獨孤發表於2023-11-26

MVC 的過濾器(Filters)也翻譯為“篩選器”。但是老周更喜歡翻譯為“過濾器”,意思上更好理解。

既然都叫過濾器了,就是在MVC的操作方法呼叫前後進行特殊處理的型別。比如:

a、此呼叫是否已授權?

b、在模型繫結之前要不要修改資料來源?(可能含有兒童不宜的資料)

c、在呼叫MVC方法前要不要改一改輸入引數?在MVC方法呼叫之後要不要處理一下結果(加點味精,進一步調味)

d、發生異常後怎麼處理?

過濾器可解決上面一堆提問。

在 ASP.NET Core 的 MVC 框架中,所有過濾器都實現共同介面 IFilterMetadata。該介面空空如也,未定義任何成員。說白了,它的用處是作為一種“記號”。你怎麼證明你就是過濾器,嗯,看看你實現了 IFilterMetadata 介面沒?實現了就認定是過濾器。所以,該介面純粹是個角色標籤。

我們們寫程式碼一般不會實現 IFilterMetadata 介面,畢竟裡面什麼卵方法都沒有,怎麼規範型別?因此,過濾器專屬名稱空間 Microsoft.AspNetCore.Mvc.Filters 下為我們公開了以下介面,方便開發者實現:

1、IAuthorizationFilter:授權過濾器,它的優先順序最高,總是最先執行。看看你有沒有許可權呼叫 MVC 方法,若沒許可權,就 See you La La。

2、IResourceFilter:資源過濾器。它在授權過濾成功後、模型繫結前執行。可以檢查一下用於繫結的資料,要不要改一下。

3、IActionFilter:操作方法過濾器,就是針對 MVC Action 的。在操作方法執行前後執行,可以用來修改輸入引數值。

4、IResultFilter:結果過濾器。當 MVC 操作方法執行成功後就會執行,可以用來修改執行結果。比如加點 HTTP 訊息頭什麼的。

5、IExceptionFilter:當 MVC 操作方法執行過程中發生異常才會執行,無異常就不會執行。

…… 其實還有的,但這裡我們們先不提,免得大夥搞得頭暈。

 

過濾器不止一個,同一型別的過濾還可能有多個,因此,它們就像中介軟體那樣,一個個連結起來,形成下水溝,哦不,是呼叫管道,或叫呼叫棧。於是,這就出現誰先執行的問題,雖然上面的介紹有說明,不過那太抽象了。任何程式設計知識只要能用程式碼來驗證和觀察,就不用圖表和理論。

下面,我們們實現上述幾個介面,然後往控制檯上列印一些文字,來看看這些過濾器是怎麼執行的。

public class CustAuthFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        Console.WriteLine("授權過濾器執行");
    }
}

public class CustResourceFilter : IResourceFilter
{
    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        Console.WriteLine("資源過濾器 - " + $"{nameof(OnResourceExecuted)}方法執行");
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        Console.WriteLine("資源過濾器 - " + $"{nameof(OnResourceExecuting)}方法執行");
    }
}

public class CustActionFilter : IActionFilter
{
    public void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine("操作過濾器 - " + $"{nameof(OnActionExecuted)}方法執行");
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("操作過濾器 - " + $"{nameof(OnActionExecuting)}方法執行");
    }
}

public class CustResultFilter : IResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("結果過濾器 - " + $"{nameof(OnResultExecuted)}方法執行");
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        Console.WriteLine("結果過濾器 - " + $"{nameof(OnResultExecuting)}方法執行");
    }
}

這裡我沒有實現異常過濾器,只實現了授權、資源、操作方法、結果這幾個有代表性的。

授權過濾器只要實現 OnAuthorization 方法即可。在實現程式碼中,可以透過 HttpContext 物件查詢授權有關的物件,如果確認是沒有訪問許可權的,可以設定一個自己定 Result 讓 MVC 操作方法的呼叫終止。

資源過濾器要實現兩個方法:OnResourceExecuting 方法在模型繫結前呼叫,這時你有機會修改資料來源;OnResourceExecuted 方法是在資源過濾之後的其他過濾器執行結束才被呼叫,即:

ResourceExecuting
    ........ 剩餘過濾器.......
ResourceExecuted

Action 過濾器也要實現兩個方法:OnActionExecuting 在操作呼叫前執行;OnActionExecuted 是在操作方法呼叫後執行。

結果過濾器需要實現兩個方法:OnResultExecuting 方法在操作結果執行前呼叫,這裡可以修改 MVC 方法返回的值;OnResultExecuted 方法是在操作結果執行之後呼叫,一般這裡可以改改HTTP嚮應頭、Cookie 什麼的。

其實我們們剛實現的過濾器都是同步版本,這些過濾器都有配套的非同步版本,介面都是以 IAsync 開頭。這裡我們們先不用管同步非同步,避免搞得複雜了。也不要去理會過濾器是全域性的還是區域性的,下面我們們統一把它們註冊為全域性的。配置方法是透過 MVC 選項類的 Filters 集合,把要用的過濾器新增進去即可。

 var builder = WebApplication.CreateBuilder(args);
 builder.Services.AddControllersWithViews(options =>
 {
     // 配置全域性過濾器
     options.Filters.Add<CustAuthFilter>();
     options.Filters.Add<CustResourceFilter>();
     options.Filters.Add<CustActionFilter>();
     options.Filters.Add<CustResultFilter>();
 });
 var app = builder.Build();

新增一個“狗頭”控制器,用於測試。

public class GouTouController : Controller
{
    public IActionResult Index()
    {
        Console.WriteLine("Index操作執行");
        return View();
    }
}

 

為了防止 ASP.NET Core 應用程式輸出的日誌干擾我們們檢視控制檯內容,我們們禁用所有日誌輸出。開啟 appsettings.json 檔案,把所有日誌類別的記錄級別改為 None。

{
  "Logging": {
    "LogLevel": {
      "*": "None"
    }
  },
  "AllowedHosts": "*"
}

星號 * 的意思就是代表所有類別的日誌,LogLevel 為 None 就不會輸出日誌了(貌似有個別日誌禁用不了)。

執行程式後,控制檯列印出這樣的內容:

 這個流程現在是不是很清晰了?我們們畫圖表了,直接這樣表達就好:

Author
Resource Executing
    Action Executing
            Action Running
    Action Executed
    Result Executing
            Result Running
    Result Executed
Resource Executed

 

區域性過濾器的執行過程與全域性過濾器相同,如果區域性和全域性過濾器同時使用,那會發生什麼呢?我們們試試。

接下來我們為授權過濾、資源過濾、操作過濾、結果過濾各建立兩個類——用於區域性和全域性。實際開發中一般不需要這樣搞,通常全域性和區域性寫一個類就行,畢竟過濾器型別在全域性和區域性是通用的。我這裡只為了演示。區域性過濾器是透過特性類的方式應用到 MVC 方法上的,所以,區域性過濾器除了實現過濾器介面,還要從 Attribute 類派生。

1、實現區域性、全域性授權過濾器。

// 授權過濾器-區域性
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class MyAuthorFilterAttribute : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        Console.WriteLine("區域性:授權過濾器執行");
    }
}

// 授權過濾器-全域性
public class GlobAuthorFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        Console.WriteLine("全域性:授權過濾器執行");
    }
}

2、實現區域性、全域性資源過濾器。

// 資源過濾器-區域性
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class MyResourceFilterAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        Console.WriteLine("區域性:資源過濾器-Executed");
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        Console.WriteLine("區域性:資源過濾器-Executing");
    }
}

// 資源過濾器-全域性
public class GlobResourceFilter : IResourceFilter
{
    public void OnResourceExecuted(ResourceExecutedContext context)
    {
        Console.WriteLine("全域性:資源過濾器-Executed");
    }

    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        Console.WriteLine("全域性:資源過濾器-Executing");
    }
}

3、實現區域性、全域性操作過濾器。

// 操作過濾器-區域性
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class MyActionFilterAttribute : Attribute, IActionFilter
{
    public void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine("區域性:操作過濾器-Executed");
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("區域性:操作過濾器-Executing");
    }
}

// 操作過濾器-全域性
public class GlobActionFilter : IActionFilter
{
    public void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine("全域性:操作過濾器-Executed");
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("全域性:操作過濾器-Executing");
    }
}

4、實現區域性、全域性結果過濾器。

// 結果過濾器-區域性
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class MyResultFilterAttribute : Attribute, IResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("區域性:結果過濾器-Executed");
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        Console.WriteLine("區域性:結果過濾器-Executing");
    }
}

// 結果過濾器-全域性
public class GlobResultFilter : IResultFilter
{
    public void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("全域性:結果過濾器-Executed");
    }

    public void OnResultExecuting(ResultExecutingContext context)
    {
        Console.WriteLine("全域性:結果過濾器-Executing");
    }
}

 

先註冊全域性過濾器。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
    // 新增全域性過濾器
    options.Filters.Add<GlobActionFilter>();
    options.Filters.Add<GlobAuthorFilter>();
    options.Filters.Add<GlobResourceFilter>();
    options.Filters.Add<GlobResultFilter>();
});
var app = builder.Build();

區域性過濾器以特性方式應用於 MVC 操作方法。

public class SpiderController : ControllerBase
{
    [MyResourceFilter]
    [MyResultFilter]
    [MyActionFilter, MyAuthorFilter]
    public IActionResult Index()
    {
        Console.WriteLine("Index操作被呼叫");
        return Content("大火燒了毛毛蟲");
    }
}

和上一個例子一樣,禁用日誌輸出(appsettings.json檔案)。

{
  "Logging": {
    "LogLevel": {
      "*": "None"
    }
  },
  ……
}

程式執行後,控制檯列印以下內容:

 過濾器按 授權->資源->操作->結果 執行的次序不變。在同種過濾器中,全域性過濾器優先執行。

全域性授權過濾器
區域性授權過濾器
全域性資源過濾器 - 前
    區域性資源過濾器 - 前
        全域性操作過濾器 - 前
            區域性操作過濾器 - 前
                【呼叫 MVC 操作方法】
            區域性操作過濾器 - 後
        全域性操作過濾器 - 後
        全域性結果過濾器 - 前
             區域性結果過濾器 - 前
                【執行操作結果】
             區域性結果過濾器 - 後
        全域性結果過濾器 - 後
    區域性資源過濾器 - 後
全域性資源過濾器 - 後

 

另外,有一件事要注意:如果你的控制器的基類是 Controller,那麼,還有優先更高的 Action Filter。看看 Controller 類它實現了啥介面。

public abstract class Controller : ControllerBase, IActionFilter, IFilterMetadata, IAsyncActionFilter, IDisposable

我們們把剛才的控制器程式碼改一下,讓它繼承 Controller 類,並重寫 OnActionExecuting、OnActionExecuted 方法。

public class SpiderController : Controller
{
    ……

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("控制器實現的操作過濾器-Executing");
        base.OnActionExecuting(context);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine("控制器實現的操作過濾器-Executed");
        base.OnActionExecuted(context);
    }
}

然後再次執行程式,控制檯將列印以下內容:

看,這個由控制器類實現的 Action 過濾器比全域性的還早執行。