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


注:本文隸屬於《理解ASP.NET Core》系列文章,請查看置頂博客或點擊此處查看全文目錄

Filter概覽

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

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

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

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

  • 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);
    });
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM