注:本文隸屬於《理解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
- Controller和Razor Page過濾器的 OnActionExecuting
- 全域性過濾器的 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執行、異常過濾器、結果過濾器以及結果執行。
通過實現IResourceFilter
或IAsyncResourceFilter
介面:
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
,有兩種方式來處理異常:
- 將
Exception
或ExceptionDispatchInfo
置為null
- 將
ExceptionHandled
置為true
單純的僅設定Result
是行不通的。所以我建議大家,在處理異常時,除了設定Result
外,也將ExceptionHandled
設定為true
,這樣也讓讀程式碼的人更容易理解程式碼邏輯。
另外,ResourceExecutedContext.Canceled
,用於指示Action的執行是否已取消。當在 OnResourceExecuting 中手動設定 ResourceExecutingContext.Result 時,會將 Canceled 置為 true。需要注意的是,想要測試這種情況,至少要註冊兩個資源過濾器,並且在第二個資源過濾器中設定Result,才能夠在第一個過濾器中看到效果。
Action Filters
操作過濾器,在模型繫結後執行,該過濾器同樣包含“之前”和“之後”兩個行為,包裹了Action的執行(不包含Controller的建立)。
如果Action執行過程中或後續操作過濾器中丟擲異常,首先捕獲到異常的是操作過濾器的OnActionExecuted
,而不是異常過濾器。
通過實現IActionFilter
或IAsyncActionFilter
介面:
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建立時丟擲異常,那首先捕獲到異常的就是異常過濾器了。
我知道大家在初時異常過濾器的時候,有的人會誤認為它可以捕獲程式中的任何異常,這是不對的!
異常過濾器:
- 通過實現介面
IExceptionFilter
或IAsyncExceptionFilter
來自定義異常過濾器 - 可以捕獲Controller建立時(也就是隻捕獲建構函式中丟擲的異常)、模型繫結、Action Filter和Action中丟擲的未處理異常
- 其他地方丟擲的異常不會捕獲
先來看一下這兩個介面:
// 僅具有標記作用,標記其為 mvc 請求管道的過濾器
public interface IFilterMetadata { }
public interface IExceptionFilter : IFilterMetadata
{
// 當丟擲異常時,該方法會捕獲
void OnException(ExceptionContext context);
}
public interface IAsyncExceptionFilter : IFilterMetadata
{
// 當丟擲異常時,該方法會捕獲
Task OnExceptionAsync(ExceptionContext context);
}
OnException
和OnExceptionAsync
方法都包含一個型別為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結果的序列化操作等。
通過實現IResultFilter
或IAsyncResultFilter
介面:
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!");
}
}
}
上面說過,IResultFilter
或IAsyncResultFilter
介面有一定的侷限性,當授權、資源過濾器使管道短路或異常過濾器通過生成Result來處理異常等,會導致結果過濾器不被執行。但是,如果在這種情況下,我們也想要執行結果過濾器,那該咋辦呢?別慌,ASP.NET Core已經想到這種情況了。
那就是實現IAlwaysRunResultFilter
或IAsyncAlwaysRunResultFilter
介面,看這名字就夠直接了吧——始終執行:
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);
});
}