理解ASP.NET Core - 過濾器(Filters)

xiaoxiaotank發表於2021-11-30

注:本文隸屬於《理解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄

Filter概覽

如果你是從ASP.NET一路走過來的,那麼你一定對過濾器(Filter)不陌生。當然,ASP.NET Core仍然繼承了過濾器機制。

過濾器執行在過濾器管道中,這是一張官方的圖,很好地解釋了過濾器管道在HTTP請求管道中的位置:

理解ASP.NET Core - 過濾器(Filters)

可以看到,只有當路由選擇了MVC Action之後,過濾器管道才有機會執行。

過濾器不止一種,而是有多種型別。為了讓各位對各過濾器執行順序更容易理解一下,我把官方的圖魔改了一下,如下:

理解ASP.NET Core - 過濾器(Filters)
  • Authorization Filters(授權過濾器):該過濾器位於所有過濾器的頂端,首先被執行。授權過濾器用於確認請求使用者是否已授權,如未授權,則可以將管道短路,禁止請求繼續傳遞。
  • Resource Filters(資源過濾器):當授權通過後,可以在過濾器管道的其他階段(如模型繫結)之前和之後執行自定義邏輯
  • Action Filters(操作過濾器):在呼叫Action之前和之後執行自定義邏輯。通過操作過濾器,可以修改要傳入Action的引數,也可以設定或修改Action的返回結果。另外,其也可以首先捕獲到Action中丟擲的未處理異常並進行處理。
  • Exception Filters(異常過濾器):當Controller建立時、模型繫結、Action Filters和Action執行中丟擲未處理的異常時,異常過濾器可以捕獲並進行處理,需要注意的是,在此之前,響應正文還未被寫入,這意味著你可以設定返回結果。
  • Result Filters(結果過濾器):僅當Action的執行未丟擲異常,或Action Filter處理了異常時,才會執行結果過濾器,允許你在操作結果執行之前和之後執行自定義邏輯。

東西有點多,你要忍一下。等看過下面的詳細介紹之後,再來回顧上面,就很容易理解了。

這些過濾器,均實現了IFilterMetadata介面,該介面不包含任何行為,僅僅是用於標記這是MVC請求管道中的過濾器。

另外,如Resource Filters、Action Filters和Result Filters這種,他們擁有兩個行為,分別在管道階段的之前和之後執行,並按照習慣,將之前命名為OnXXXing,如 OnActionExecuting,將之後命名為OnXXXExecuted,如 OnActionExecuted

過濾器的作用域和註冊方式

由於過濾器的種類繁多,為了方便大家邊學習邊測試,所以先介紹一下過濾器的作用域和註冊方式。

過濾器的作用域範圍和執行順序

同樣的,在介紹過濾器之前,先給大家介紹一下過濾器的作用域範圍和執行順序。

過濾器的作用域範圍,可分為三種,從小到大是:

  • 某個Controller中的某個Action上(不支援Razor Page中的處理方法)
  • 某個Controller或Razor Page上
  • 全域性,應用到所有Controller、Action和Razor Page上

不同過濾器的執行順序,我們通過上面那幅圖可以很清楚的知曉了,但是對於不同作用域的同一型別的過濾器,執行順序又是怎樣的呢?

IActionFilter舉例說明,執行順序為:

  • 全域性過濾器的 OnActionExecuting
    • Controller和Razor Page過濾器的 OnActionExecuting
      • Action過濾器的 OnActionExecuting
      • Action過濾器的 OnActionExecuted
    • Controller和Razor Page過濾器的 OnActionExecuted
  • 全域性過濾器的 OnActionExecuted

也就是說,對於不同作用域的同一型別的過濾器,執行順序是由作用域範圍大到小,然後再由小到大

過濾器的註冊方式

接下來,看一下如何將過濾器註冊為不同的作用域:

全域性

註冊為全域性比較簡單,直接配置MvcOptions.Filters即可:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options => options.Filters.Add<MyFilter>());
    // or
    services.AddControllers(options => options.Filters.Add<MyFilter>());
    // or
    services.AddControllersWithViews(options => options.Filters.Add<MyFilter>());
}

Controller、Razor Page 或 Action

作用域為 Controller、Razor Page 或 Action 在註冊方式上來說,實際上都是差不多的,都是以特性的方式進行標註。

最簡單的,過濾器建構函式無引數或這些引數均無需由DI來提供,此時只需要過濾器繼承Attribute即可:

class MyFilterAttribute : Attribute, IActionFilter { }

[MyFilter]
public class HomeController : Controller { }

另一種,過濾器的建構函式引數均需要DI來提供,此時就需要用到ServiceFilterAttribute了:

class MyFilter :IActionFilter
{
    public MyFilter(IWebHostEnvironment env) { }
}

public void ConfigureServices(IServiceCollection services)
{
    // 將過濾器新增到 DI 容器
    services.AddScoped<MyFilter>();
}

[ServiceFilter(typeof(MyFilter))]
public class HomeController : Controller { }

ServiceFilterAttribute是如何建立這種型別過濾器的例項的呢?看下它的結構你就明白了:

public interface IFilterFactory : IFilterMetadata
{
    // 過濾器例項是否可跨請求重用
    bool IsReusable { get; }

    // 通過 IServiceProvider 建立指定過濾器型別的例項
    IFilterMetadata CreateInstance(IServiceProvider serviceProvider);
}

public class ServiceFilterAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
    // type 就是要建立的過濾器的型別
    public ServiceFilterAttribute(Type type) 
    {
        ServiceType = type ?? throw new ArgumentNullException(nameof(type)); 
    }

    public int Order { get; set; }

    // 獲取過濾器的型別,也就是建構函式中傳進來的
    public Type ServiceType { get; }

    // 過濾器例項是否可跨請求重用,預設 false
    public bool IsReusable { get; set; }

    // 通過 IServiceProvider.GetRequiredService 建立指定過濾器型別的例項
    // 所以要求該過濾器和建構函式引數要在DI容器中註冊
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) 
    {
        var filter = (IFilterMetadata)serviceProvider.GetRequiredService(ServiceType);
        if (filter is IFilterFactory filterFactory)
        {
            // 展開 IFilterFactory
            filter = filterFactory.CreateInstance(serviceProvider);
        }

        return filter;
    }
}

如果你想要使過濾器例項在其作用域之外被重用,可以通過指定IsReusable = true來達到目的,需要注意的是要保證該過濾器所依賴的服務生命週期一定是單例的。另外,這並不能保證該過濾器例項是單例,也有可能出現多個。

好了,還有最後一種最複雜的,就是過濾器的建構函式部分不需要DI來提供,部分又需要DI來提供,此時就需要用到TypeFilterAttribute了:

class MyFilter : IActionFilter
{
    // 第一個引數 caller 不是通過DI提供的
    // 第二個引數 env 是通過DI提供的
    public MyFilter(string caller, IWebHostEnvironment env) { }
}

// ... 注意,這裡就不需要將 MyFilter 註冊到DI容器了,記得將註冊程式碼刪除

// Arguments 裡面存放的引數就是無需DI提供的引數
[TypeFilter(typeof(MyFilter), 
    Arguments = new object[] { "HomeController" })]
public class HomeController : Controller { }

同樣,看一下TypeFilterAttribute的結構:

public class TypeFilterAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
    private ObjectFactory _factory;

    // type 就是要建立的過濾器的型別
    public TypeFilterAttribute(Type type) 
    { 
        ImplementationType = type ?? throw new ArgumentNullException(nameof(type));
    }

    // 要傳遞給過濾器建構函式的非DI容器提供的引數
    public object[] Arguments { get; set; }

    // 獲取過濾器的型別,也就是建構函式中傳進來的
    public Type ImplementationType { get; }

    public int Order { get; set; }

    public bool IsReusable { get; set; }

    // 通過 ObjectFactory 建立指定過濾器型別的例項
    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) 
    { 
        if (_factory == null)
        {
            var argumentTypes = Arguments?.Select(a => a.GetType())?.ToArray();
            _factory = ActivatorUtilities.CreateFactory(ImplementationType, argumentTypes ?? Type.EmptyTypes);
        }

        var filter = (IFilterMetadata)_factory(serviceProvider, Arguments);
        if (filter is IFilterFactory filterFactory)
        {
            // 展開 IFilterFactory
            filter = filterFactory.CreateInstance(serviceProvider);
        }

        return filter;
    }
}

過濾器上下文

過濾器中的行為,都會有一個上下文引數,這些上下文引數都繼承自抽象類FilterContext,而FilterContext又繼承自ActionContext(這也從側面說明了,過濾器就是為Action服務的):

public class ActionContext
{
    // Action相關的資訊
    public ActionDescriptor ActionDescriptor { get; set; }

    // HTTP上下文
    public HttpContext HttpContext { get; set; }

    // 模型繫結和驗證
    public ModelStateDictionary ModelState { get; }

    // 路由資料
    public RouteData RouteData { get; set; }
}

public abstract class FilterContext : ActionContext
{
    public virtual IList<IFilterMetadata> Filters { get; }

    public bool IsEffectivePolicy<TMetadata>(TMetadata policy) where TMetadata : IFilterMetadata {}

    public TMetadata FindEffectivePolicy<TMetadata>() where TMetadata : IFilterMetadata {}
}

當我們自定義一個過濾器時,免不了要和上下文進行互動,所以,瞭解上下文的結構,是不可或缺的。下面就挑兩個重要的引數探究一下。

我們先來看ActionDescriptor,它裡面包含了和Action相關的資訊:

public class ActionDescriptor
{
    // 標識該Action的唯一標識,其實就是一個Guid
    public string Id { get; }

    // 路由字典,包含了controller、action的名字等
    public IDictionary<string, string> RouteValues { get; set; }

    // 特性路由的相關資訊
    public AttributeRouteInfo? AttributeRouteInfo { get; set; }

    // Action的約束列表
    public IList<IActionConstraintMetadata>? ActionConstraints { get; set; }

    // 終結點後設資料,我們們一般用不到
    public IList<object> EndpointMetadata { get; set; }

    // 路由中的引數列表,包含引數名、引數型別、繫結資訊等
    public IList<ParameterDescriptor> Parameters { get; set; }

    public IList<ParameterDescriptor> BoundProperties { get; set; }

    // 過濾器管道中與當前Action有關的過濾器列表
    public IList<FilterDescriptor> FilterDescriptors { get; set; }

    // Action的個性化名稱
    public virtual string? DisplayName { get; set; }

    // 共享後設資料
    public IDictionary<object, object> Properties { get; set; }
}

下面的HttpContext這個就不說了,太大了。不過你得知道,有了它,你可以針對請求和響應做自己想做的操作。

接下來就是ModelState,它是用於校驗模型繫結的,通過它,可以知道模型是否繫結成功,也可以得到繫結失敗的校驗資訊。相關細節將在後續關於模型繫結的文章中進行介紹。

然後就是RouteData,很顯然,它儲存了和路由有關的資訊,那就看一下它包括什麼吧:

public class RouteData
{
    // 當前路由路徑上由路由生成的資料標記
    public RouteValueDictionary DataTokens { get; }

    // Microsoft.AspNetCore.Routing.IRouter 的例項列表
    public IList<IRouter> Routers { get; }

    // 路由值,包含了 ActionDescriptor.RouteValues 中的資料
    public RouteValueDictionary Values { get; }
}

後面,就來到了Filters,看到IFilterMetadata我相信你也已經猜到了,它表示過濾器管道中與當前Action有關的過濾器列表。

Authorization Filters

授權過濾器是過濾器管道的第一個被執行的過濾器,用於系統授權。一般不會編寫自定義的授權過濾器,而是配置授權策略或編寫自定義授權策略。詳細內容將在後續文章介紹。

Resource Filters

資源過濾器,在授權過濾器執行後執行,該過濾器包含“之前”和“之後”兩個行為,包裹了模型繫結、操作過濾器、Action執行、異常過濾器、結果過濾器以及結果執行。

通過實現IResourceFilterIAsyncResourceFilter介面:

public interface IResourceFilter : IFilterMetadata
{
    void OnResourceExecuting(ResourceExecutingContext context);

    void OnResourceExecuted(ResourceExecutedContext context);
}

public interface IAsyncResourceFilter : IFilterMetadata
{
    Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next);
}

當攔截到請求時,你可以得到資源資訊的上下文:

public class ResourceExecutingContext : FilterContext
{
    // 獲取或設定該Action的執行結果
    public virtual IActionResult? Result { get; set; }

    // Action引數繫結源提供器工廠,比如 Form、Route、QueryString、JQueryForm、FormFile等
    public IList<IValueProviderFactory> ValueProviderFactories { get; }
}

public class ResourceExecutedContext : FilterContext
{
    // 指示Action的執行是否已取消
    public virtual bool Canceled { get; set; }

    // 如果捕獲到未處理的異常,會存放到此處
    public virtual Exception? Exception { get; set; }

    public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }

    // 指示異常是否已被處理
    public virtual bool ExceptionHandled { get; set; }

    // 獲取或設定該Action的執行結果
    public virtual IActionResult? Result { get; set; }
}

類似的,一旦設定了Result,就可以使過濾器管道短路。

對於ResourceExecutedContext,有兩種方式來處理異常:

  • ExceptionExceptionDispatchInfo置為null
  • ExceptionHandled置為true

單純的僅設定Result是行不通的。所以我建議大家,在處理異常時,除了設定Result外,也將ExceptionHandled設定為true,這樣也讓讀程式碼的人更容易理解程式碼邏輯。

另外,ResourceExecutedContext.Canceled,用於指示Action的執行是否已取消。當在 OnResourceExecuting 中手動設定 ResourceExecutingContext.Result 時,會將 Canceled 置為 true。需要注意的是,想要測試這種情況,至少要註冊兩個資源過濾器,並且在第二個資源過濾器中設定Result,才能夠在第一個過濾器中看到效果。

Action Filters

操作過濾器,在模型繫結後執行,該過濾器同樣包含“之前”和“之後”兩個行為,包裹了Action的執行(不包含Controller的建立)。

如果Action執行過程中或後續操作過濾器中丟擲異常,首先捕獲到異常的是操作過濾器的OnActionExecuted,而不是異常過濾器。

通過實現IActionFilterIAsyncActionFilter介面:

public interface IActionFilter : IFilterMetadata
{
    void OnActionExecuting(ActionExecutingContext context);

    void OnActionExecuted(ActionExecutedContext context);
}

public interface IAsyncActionFilter : IFilterMetadata
{
    Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next);
}

同樣地,看一下上下文結構:

public class ActionExecutingContext : FilterContext
{
    // 獲取或設定該Action的執行結果
    public virtual IActionResult? Result { get; set; }

    // Action的引數字典,key是引數名,value是引數值
    public virtual IDictionary<string, object> ActionArguments { get; }

    // 獲取該Action所屬的Controller
    public virtual object Controller { get; }
}

public class ActionExecutedContext : FilterContext
{
    // 指示Action的執行是否已取消
    public virtual bool Canceled { get; set; }

    // 獲取該Action所屬的Controller
    public virtual object Controller { get; }

    // 如果捕獲到未處理的異常,會存放到此處
    public virtual Exception? Exception { get; set; }

    public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }

    // 指示異常是否已被處理
    public virtual bool ExceptionHandled { get; set; }

    // 獲取或設定該Action的執行結果
    public virtual IActionResult Result { get; set; }
}

關於ActionExecutedContext.Canceled屬性和異常處理相關的知識點,均與資源過濾器類似,這裡就不再贅述了。

由於操作過濾器常常在應用中的使用比較頻繁,所以這裡詳細介紹一下它的使用。ASP.NET Core框架提供了一個抽象類ActionFilterAttribute,該抽象類實現了多個介面,還繼承了Attribute,允許我們以特性的方式使用。所以,一般比較建議大家通過繼承該抽象類來自定義操作過濾器:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public abstract class ActionFilterAttribute :
    Attribute, IActionFilter, IAsyncActionFilter, IResultFilter, IAsyncResultFilter, IOrderedFilter
{
    public int Order { get; set; }

    public virtual void OnActionExecuting(ActionExecutingContext context) { }

    public virtual void OnActionExecuted(ActionExecutedContext context) { }

    public virtual async Task OnActionExecutionAsync(
        ActionExecutingContext context,
        ActionExecutionDelegate next)
    {
        // 刪除了一些空校驗程式碼...

        OnActionExecuting(context);
        if (context.Result == null)
        {
            OnActionExecuted(await next());
        }
    }

    public virtual void OnResultExecuting(ResultExecutingContext context) { }

    public virtual void OnResultExecuted(ResultExecutedContext context) { }

    public virtual async Task OnResultExecutionAsync(
        ResultExecutingContext context,
        ResultExecutionDelegate next)
    {
        // 刪除了一些空校驗程式碼...

        OnResultExecuting(context);
        if (!context.Cancel)
        {
            OnResultExecuted(await next());
        }
    }
}

可以看到,ActionFilterAttribute同時實現了同步和非同步介面,不過,我們在使用時,只需要實現同步或非同步介面就可以了,不要同時實現。這是因為,執行時會先檢查過濾器是否實現了非同步介面,如果是,則呼叫該非同步介面。否則,就呼叫同步介面。 如果在一個類中同時實現了非同步和同步介面,則僅會呼叫非同步介面。

當要全域性進行驗證模型繫結狀態時,使用操作過濾器再合適不過了!

public class ModelStateValidationFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            if (context.HttpContext.Request.AcceptJson())
            {
                var errorMsg = string.Join(Environment.NewLine, context.ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
                context.Result = new BadRequestObjectResult(AjaxResponse.Failed(errorMsg));
            }
            else
            {
                context.Result = new ViewResult();
            }
        }
    }
}

public static class HttpRequestExtensions
{
    public static bool AcceptJson(this HttpRequest request)
    {
        if (request == null) throw new ArgumentNullException(nameof(request));

        var regex = new Regex(@"^(\*|application)/(\*|json)$");

        return request.Headers[HeaderNames.Accept].ToString()
            .Split(',')
            .Any(type => regex.IsMatch(type));
    }
}

Exception Filters

異常過濾器,可以捕獲Controller建立時(也就是隻捕獲建構函式中丟擲的異常)、模型繫結、Action Filter和Action中丟擲的未處理異常。

再著重說明一下:如果Action執行過程中或非首個操作過濾器中丟擲異常,首先捕獲到異常的是操作過濾器的OnActionExecuted,而不是異常過濾器。但是,如果在Controller建立時丟擲異常,那首先捕獲到異常的就是異常過濾器了。

我知道大家在初時異常過濾器的時候,有的人會誤認為它可以捕獲程式中的任何異常,這是不對的!

異常過濾器:

  • 通過實現介面IExceptionFilterIAsyncExceptionFilter來自定義異常過濾器
  • 可以捕獲Controller建立時(也就是隻捕獲建構函式中丟擲的異常)、模型繫結、Action Filter和Action中丟擲的未處理異常
  • 其他地方丟擲的異常不會捕獲

先來看一下這兩個介面:

// 僅具有標記作用,標記其為 mvc 請求管道的過濾器
public interface IFilterMetadata { }

public interface IExceptionFilter : IFilterMetadata
{
    // 當丟擲異常時,該方法會捕獲
    void OnException(ExceptionContext context);
}

public interface IAsyncExceptionFilter : IFilterMetadata
{
    // 當丟擲異常時,該方法會捕獲
    Task OnExceptionAsync(ExceptionContext context);
}

OnExceptionOnExceptionAsync方法都包含一個型別為ExceptionContext引數,很顯然,它就是與異常有關的上下文,我們的異常處理邏輯離不開它。那接著來看一下它的結構吧:

public class ExceptionContext : FilterContext
{
    // 捕獲到的未處理異常
    public virtual Exception Exception { get; set; }

    public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }

    // 指示異常是否已被處理
    // true:表示異常已被處理,異常不會再向上丟擲
    // false:表示異常未被處理,異常仍會繼續向上丟擲
    public virtual bool ExceptionHandled { get; set; }

    // 設定響應的 IActionResult
    // 如果設定了結果,也表示異常已被處理,異常不會再向上丟擲
    public virtual IActionResult? Result { get; set; }
}

下面,我們就來實現一個自定義的異常處理器:

public class MyExceptionFilterAttribute : ExceptionFilterAttribute
{
    private readonly IModelMetadataProvider _modelMetadataProvider;

    public MyExceptionFilterAttribute(IModelMetadataProvider modelMetadataProvider)
    {
        _modelMetadataProvider = modelMetadataProvider;
    }

    public override void OnException(ExceptionContext context)
    {
        if (!context.ExceptionHandled)
        {
            // 此處僅為簡單演示
            var exception = context.Exception;
            var result = new ViewResult()
            {
                ViewName = "Error",
                ViewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState)
                {
                    // 記得給 ErrorViewModel 加上 Message 屬性
                    Model = new ErrorViewModel
                    {
                        Message = exception.ToString()
                    }
                }
            };

            context.Result = result;

            // 標記異常已處理
            context.ExceptionHandled = true;
        }
    }
}

接著,找到/Views/Shared/Error.cshtml,展示一下錯誤訊息:

@model ErrorViewModel
@{
    ViewData["Title"] = "Error";
}

<p>@Model.Message</p>

最後,註冊一下MyExceptionFilterAttribute

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<MyExceptionFilterAttribute>();

    services.AddControllersWithViews();
}

現在,我們將該異常處理器加在/Home/Index上,並拋個異常:

public class HomeController : Controller
{
    [ServiceFilter(typeof(MyExceptionFilterAttribute))]
    public IActionResult Index()
    {
        throw new Exception("Home Index Error");

        return View();
    }
}

當請求/Home/Index時,你會得到如下頁面:

Result Filters

結果過濾器,包裹了操作結果的執行。所謂操作結果的執行,可以是Razor檢視的處理操作,也可以是Json結果的序列化操作等。

通過實現IResultFilterIAsyncResultFilter介面:

public interface IResultFilter : IFilterMetadata
{
    void OnResultExecuting(ResultExecutingContext context);

    void OnResultExecuted(ResultExecutedContext context);
}

public interface IAsyncResultFilter : IFilterMetadata
{
    Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next);
}

當實現這兩個介面其一時,則僅當Action或Action Filters生成Result時,才會執行結果過濾器。像授權、資源過濾器使管道短路或異常過濾器通過生成Result來處理異常等,都不會執行結果過濾器。

如果在 OnResultExecuting 中拋異常了,就會導致短路,Action結果和後續的結果過濾器都不會執行,並且執行結果也被視為失敗。

同樣地,看一下上下文結構:

public class ResultExecutingContext : FilterContext
{
    // 獲取該Action所屬的Controller
    public virtual object Controller { get; }

    // 獲取或設定該Action的結果
    public virtual IActionResult Result { get; set; }

    // 指示結果過濾器是否應該被短路,若短路,Action結果和後續的的結果過濾器,都不會執行
    public virtual bool Cancel { get; set; }
}

public class ResultExecutedContext : FilterContext
{
    // 指示結果過濾器是否被短路,若短路,Action結果和後續的的結果過濾器,都不會執行
    public virtual bool Canceled { get; set; }

    // 獲取該Action所屬的Controller
    public virtual object Controller { get; }

    // 獲取或設定結果或結果過濾器執行過程中丟擲的未處理異常
    public virtual Exception? Exception { get; set; }

    public virtual ExceptionDispatchInfo? ExceptionDispatchInfo { get; set; }

    // 異常是否已被處理
    public virtual bool ExceptionHandled { get; set; }

    // 獲取或設定該Action的執行結果
    public virtual IActionResult Result { get; }
}

可以通過繼承抽象類ResultFilterAttribute來實現自定義結果過濾器:

class MyResultFilter : ResultFilterAttribute
{
    private readonly ILogger<MyResultFilter> _logger;

    public MyResultFilter(ILogger<MyResultFilter> logger)
    {
        _logger = logger;
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        context.HttpContext.Response.Headers.Add("CustomHeaderName", "CustomHeaderValue");
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        if (context.HttpContext.Response.HasStarted)
        {
            _logger.LogInformation("Response has started!");
        }
    }
}

上面說過,IResultFilterIAsyncResultFilter介面有一定的侷限性,當授權、資源過濾器使管道短路或異常過濾器通過生成Result來處理異常等,會導致結果過濾器不被執行。但是,如果在這種情況下,我們也想要執行結果過濾器,那該咋辦呢?別慌,ASP.NET Core已經想到這種情況了。

那就是實現IAlwaysRunResultFilterIAsyncAlwaysRunResultFilter介面,看這名字就夠直接了吧——始終執行:

public interface IAlwaysRunResultFilter : IResultFilter, IFilterMetadata { }

public interface IAsyncAlwaysRunResultFilter : IAsyncResultFilter, IFilterMetadata { }

中介軟體過濾器

中介軟體過濾器,其實是在過濾器管道中加入中介軟體管道。中介軟體過濾器的執行時機與資源過濾器一樣,即模型繫結之前和管道的其餘部分執行之後執行。

要建立中介軟體過濾器,需要滿足一個條件,那就是該中介軟體必須包含一個Configure方法(一般來說還會包含一個IApplicationBuilder引數用於配置中介軟體管道,不過這不是強制的)。

例如:

class MyPipeline
{
    public void Configure(IApplicationBuilder app)
    {
        System.Console.WriteLine("MyPipeline");
    }
}

[MiddlewareFilter(typeof(MyPipeline))]
public class HomeController : Controller { }

其他

IOrderedFilter

針對同一型別的過濾器,我們可以有多個實現,這些實現,可以註冊到不同的作用域中,而且同一個作用域可以有多個該過濾器型別的實現。如果我們將這樣的多個實現作用於同一個Action,這些過濾器例項的執行順序就是我們所要關心的了。

預設的,如果將同一作用域的同一型別的過濾器的多個實現作用到某個Action上,則這些過濾器例項的執行順序是按照註冊的順序進行的。

例如,我們現在有兩個操作過濾器——MyActionFilter1和MyActionFilter2:

public class MyActionFilter1 : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("OnActionExecuting: MyActionFilter1");
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("OnResultExecuted: MyActionFilter1");
    }
}

public class MyActionFilter2 : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("OnActionExecuting: MyActionFilter2");
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("OnResultExecuted: MyActionFilter2");
    }
}

然後將其作用到HomeController.Index方法上,並且,先註冊MyActionFilter2,再註冊MyActionFilter1:

public class HomeController : Controller
{
    [MyActionFilter2]
    [MyActionFilter1]
    public IActionResult Index()
    {
        return View();
    }
}

當請求Home/Index時,控制檯的輸出如下:

OnActionExecuting: MyActionFilter2
OnActionExecuting: MyActionFilter1
OnResultExecuted: MyActionFilter1
OnResultExecuted: MyActionFilter2

但是,我們在開發過程中,很容易手滑將註冊順序弄錯,這時我們就需要一個手動指定執行順序的機制,這就用到了IOrderedFilter介面。

public interface IOrderedFilter : IFilterMetadata
{
    // 執行順序
    int Order { get; }
}

IOrderedFilter介面很簡單,只有一個Order屬性,表示執行順序,預設值為0。Order值越小,則過濾器的Before方法越先執行,After方法越後執行。

下面我們改造一下MyActionFilter1和MyActionFilter2,讓MyActionFilter1先執行:

public class MyActionFilter1 : ActionFilterAttribute
{
    public MyActionFilter1()
    {
        Order = -1;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("OnActionExecuting: MyActionFilter1");
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("OnResultExecuted: MyActionFilter1");
    }
}

public class MyActionFilter2 : ActionFilterAttribute
{
    public MyActionFilter2()
    {
        Order = 1;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("OnActionExecuting: MyActionFilter2");
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        Console.WriteLine("OnResultExecuted: MyActionFilter2");
    }
}

此時,再次請求Home/Index,控制檯的輸出如下:

OnActionExecuting: MyActionFilter1
OnActionExecuting: MyActionFilter2
OnResultExecuted: MyActionFilter2
OnResultExecuted: MyActionFilter1

現在,我們看一下不同作用域的情況下,Order是否生效。將MyActionFilter2作用域提升到控制器上。

[MyActionFilter2]
public class HomeController : Controller
{
    [MyActionFilter1]
    public IActionResult Index()
    {
        return View();
    }
}

此時,再次請求Home/Index,控制檯的輸出如下:

OnActionExecuting: MyActionFilter1
OnActionExecuting: MyActionFilter2
OnResultExecuted: MyActionFilter2
OnResultExecuted: MyActionFilter1

哇,神奇的事情發生了,作用域為Action的MyActionFilter1竟然優先於作用域為Controller的MyActionFilter2執行。

實際上,Order會重寫作用域,即先按Order對過濾器進行排序,然後再通過作用域消除並列問題。

另外,若要始終首先執行全域性過濾器,則請將Order設定為int.MinValue

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        options.Filters.Add<MyActionFilter2>(int.MinValue);
    });
}

相關文章