0%

ASP.NET Core - Filter

Filter 可以 在Action 執行前執行後 對 Request 進行加工處理,包括錯誤處理、快取、授權等等。使用 Filter 的好處是可以避免重複的程式碼,例如,錯誤處理可以透過 Filter 合併處理錯誤。

Filter 的運作方式

Filter

Filter 會在 Middleware 執行完後執行。Filter 和 Middleware 都可以達到類似的目的,要使用哪個取決於是否需要存取 MVC context。

Middleware 只能存取 HttpContext,而 Filter 可以存取整個 MVC context,所以 Filter 可以存取 Routing data 和 model binding 的資訊。

一般來說,如果關注的點是和 MVC 無關的則可以使用 Middleware,若是需要驗證或是修改值則要使用 Filter 才有辦法進行存取。

關於 Middleware 的更多資訊請參考 ASP.NET Core - Middleware

Filter的類型

Filter 總共有以下幾種不同的類型,會依照順序執行

  • Authorization Filter :
    最先執行,驗證 Request 是否合法,若不合法則會直接回傳驗證失敗或401之類的,若是要經過邏輯運算驗證的則不能在此驗證,需使用 ActionFilter,待邏輯運算回傳後再處理。

  • Resource Filter :
    在 Authorization Filter 和其他所有 Filter 都執行完之後會執行,用來處理快取或 Model Binding,如果 Cache 中已經有需要的值那就不需要再繼續執行剩下的步驟了,也可以限制檔案上傳大小等等。

  • Action Filter :
    在 Action 前後執行,處理傳入的 Action 的參數和回應的結果。

  • Exception Filter :
    處理應用程式沒有處理的例外狀況。

  • Result Filter :
    應用程式執行無誤後才會執行,若在 Authorization、Resource 或是 Exception 的 Filter 被攔截回傳的話則不會執行 Result Filter

Filter執行順序

實作 Filter

以下以 Dotnet Core 3.1 實作,分別繼承不同類型的 Filter 的 Interface 去實作,範例皆為非同步的方法。

await next() 前代表執行前的處理,呼叫 await next() 代表呼叫執行下一個 Filter,await next() 後的則代表執行後的處理。

另外需特別注意的是如果在執行前的處理已經指派值給 context.Result,則不能再呼叫 await next()。

若要使用同步方法,則繼承的 Interface 去除 Async 即可,而同步的實作分為兩個 function,分別為 OnExecuting (執行前)和 OnExecuted (執行後)。

Authorization Filter

實作 IAsyncAuthorizationFilterIAuthorizationFilter

範例: 以 cookie 中帶 token 傳入驗證為例,若 token 驗證不過則回傳 401 未授權

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/// <summary>
/// Class AuthorizationFilter
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncAuthorizationFilter" />
public class AuthorizationFilter : IAsyncAuthorizationFilter
{
/// <summary>
/// Called early in the filter pipeline to confirm request is authorized.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext" />.</param>
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var cookies = context.HttpContext.Request.Cookies;

cookies.TryGetValue("token", out string token);

if (token.Equals("123456"))
{
var response = new FailResultViewModel
{
CorrelationId = Guid.NewGuid().ToString(),
Method = $"{context.HttpContext.Request.Path}.{context.HttpContext.Request.Method}",
Status = "UnAuthorized",
Version = "1.0",
Error = new FailInformation()
{
Domain = "ProjectName",
Message = "未授權",
Description = "授權驗證失敗"
}
};

context.Result = new ObjectResult(response)
{
// 401
StatusCode = (int)HttpStatusCode.Unauthorized
};
}
}
}

Resource Filter

實作 IAsyncResourceFilterIResourceFilter

範例 : 以 Cahce 為例,當傳入相同的 Request 時直接從 Cache 回傳結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/// <summary>
/// Class CacheResourceFilter
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncResourceFilter" />
public class CacheResourceFilter : IAsyncResourceFilter
{
private static readonly Dictionary<string, ObjectResult> _cache = new Dictionary<string, ObjectResult>();

/// <summary>
/// Called asynchronously before the rest of the pipeline.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ResourceExecutingContext" />.</param>
/// <param name="next">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ResourceExecutionDelegate" />. Invoked to execute the next resource filter or the remainder
/// of the pipeline.</param>
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
{
var cacheKey = context.HttpContext.Request.Path.ToString();

if (_cache != null && _cache.ContainsKey(cacheKey))
{
var cacheValue = _cache[cacheKey];
if (cacheValue != null)
{
context.Result = cacheValue;
}
}
else
{
var executedContext = await next();

var result = executedContext.Result as ObjectResult;
if (result != null)
{
_cache.Add(cacheKey, result);
}
}
}
}

Action Filter

實作 IAsyncActionFilterIActionFilter

這裡分別針對執行前和執行後做兩個範例

範例1 : 對執行前傳入的參數檢查,若檢查未通過則回傳參數驗證失敗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// <summary>
/// Class ValidationActionFilter
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncActionFilter" />
public class ValidationActionFilter : IAsyncActionFilter, IOrderedFilter
{
public int Order { get; set; } = 0;

/// <summary>
/// Called asynchronously before the action, after model binding is complete.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext" />.</param>
/// <param name="next">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutionDelegate" />. Invoked to execute the next action filter or the action itself.</param>
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var parameter = context.ActionArguments.SingleOrDefault();
if (parameter.Value is null)
{
var response = new FailResultViewModel
{
CorrelationId = Guid.NewGuid().ToString(),
Method = $"{context.HttpContext.Request.Path}.{context.HttpContext.Request.Method}",
Status = "Error",
Version = "1.0",
Error = new FailInformation
{
Domain = "ProjectName",
Message = "參數驗證失敗",
Description = "傳入參數為null"
}
};

context.Result = new ObjectResult(response)
{
// 400
StatusCode = (int)HttpStatusCode.BadRequest
};
}
else
{
await next();
}
}
}

範例2 : 將執行後的結果再包裝成特定格式後回傳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/// <summary>
/// Class MessageActionFilter
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncActionFilter" />
public class MessageActionFilter : IAsyncActionFilter, IOrderedFilter
{
public int Order { get; set; } = 0;

/// <summary>
/// Called asynchronously before the action, after model binding is complete.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext" />.</param>
/// <param name="next">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutionDelegate" />. Invoked to execute the next action filter or the action itself.</param>
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var executedContext = await next();

var result = executedContext.Result as ObjectResult;

// ModelStateDictionary存放錯誤訊息
if (result != null
&& !(result.Value is HttpResponseMessage)
&& !(result.Value is SuccessResultViewModel<object>)
&& !(result.Value is FailResultViewModel)
&& !(result.Value is SuccessResultViewModel<ModelStateDictionary>))
{
var responseModel = new SuccessResultViewModel<object>
{
Version = "1.0",
Method = $"{context.HttpContext.Request.Path}.{context.HttpContext.Request.Method}",
Status = "Success",
CorrelationId = Guid.NewGuid().ToString(),
Data = result.Value
};

executedContext.Result = new ObjectResult(responseModel)
{
// 200
StatusCode = (int)HttpStatusCode.OK
};
}
}
}

ActionExecutingContext 提供以下幾個參數 :

  • ActionArguments : 讀取輸入的值
  • Controller : 管理 Controller
  • Result : 設定 Result 會影響後續的 Filter 運作

ActionExecutedContext 提供以下幾個參數 :

  • Canceled : 如果 Action 被其他 Filter 攔截並回傳則為true
  • Exception : 如果 Action 或是先前的 Action Filter 拋出 Exception 則不會是 null

Exception Filter

實作 IAsyncExceptionFilterIExceptionFilter

範例: 針對 Action 拋出的例外狀況進行攔截並包裝成特定格式後回傳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/// <summary>
/// Class ExceptionFilter
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncExceptionFilter" />
public class ExceptionFilter : IAsyncExceptionFilter
{
/// <summary>
/// Called after an action has thrown an <see cref="T:System.Exception" />.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ExceptionContext" />.</param>
/// <returns>
/// A <see cref="T:System.Threading.Tasks.Task" /> that on completion indicates the filter has executed.
/// </returns>
public Task OnExceptionAsync(ExceptionContext context)
{
var response = new FailResultViewModel
{
CorrelationId = Guid.NewGuid().ToString(),
Method = $"{context.HttpContext.Request.Path}.{context.HttpContext.Request.Method}",
Status = "Error",
Version = "1.0",
Error = new FailInformation
{
Domain = "ProjectName",
ErrorCode = 40000,
Message = context.Exception.Message,
Description = context.Exception.ToString()
}
};

context.Result = new ObjectResult(response)
{
// 500
StatusCode = (int)HttpStatusCode.InternalServerError
};

// Exceptinon Filter只在ExceptionHandled=false時觸發
// 所以處理完Exception要標記true表示已處理
context.ExceptionHandled = true;

return Task.CompletedTask;
}
}

Result Filter

實作 IAsyncResultFilterIResultFilter

範例 : 將訊息加入 Header 回傳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/// <summary>
/// Class ResultFilter
/// </summary>
/// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncResultFilter" />
public class ResultFilter : IAsyncResultFilter
{
/// <summary>
/// Called asynchronously before the action result.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext" />.</param>
/// <param name="next">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ResultExecutionDelegate" />. Invoked to execute the next result filter or the result itself.</param>
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (!(context.Result is EmptyResult))
{
var headerName = "OnResultExecuting";

var headerValue = new string[] { "ResultExecuting Successfully" };

context.HttpContext.Response.Headers.Add(headerName, headerValue);

await next();

// 無法在執行後加入 Header,因為 Response 已經開始,此時 Response 可能已經到 Client 端那便無法修改了
}
else
{
// 若已經被其他 Filter 攔截回傳或是接收到的 context 是空的,則取消 Result 回傳
// 但是若提前被攔截則並不會進到 ResultFilter
context.Cancel = true;
}
}
}

Filter 的執行順序

預設註冊相同類型的 Filter 是採用先進後出,依照註冊的順序和層級都會影響執行順序。

順序 Filter 範圍 Filter Method
1 Global OnActionExecuting
2 Controller或Razor Page OnActionExecuting
3 Method OnActionExecuting
4 Method OnActionExecuted
5 Controller或Razor Page OnActionExecuted
6 Global OnActionExecuted

可以透過實作 IOrderedFilter 來更改執行的順序

1
2
3
4
5
6
7
8
9
public class ValidationActionFilter : IAsyncActionFilter, IOrderedFilter
{
public int Order { get; set; } = 0;

public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// ...
}
}

在註冊 Filter 時,填入 Order 來決定執行順序,數字越小代表越先執行

1
2
3
4
5
6
7
8
public static class FilterExtensions
{
public static void AddMessageFilter(this MvcOptions options)
{
options.Filters.Add(new ValidationActionFilter() { Order = 0 });
options.Filters.Add(new MessageActionFilter() { Order = 1 });
}
}

Filter 的使用方式

註冊全域 Filter

建立一個擴充類別,填入要註冊的 Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// Class FilterExtensions
/// </summary>
public static class FilterExtensions
{
/// <summary>
/// Adds the message filter.
/// </summary>
/// <param name="options">The options.</param>
public static void AddMessageFilter(this MvcOptions options)
{
options.Filters.Add<AuthorizationFilter>();
options.Filters.Add<CacheResourceFilter>();
options.Filters.Add<ExceptionFilter>();
options.Filters.Add(new ValidationActionFilter() { Order = 0 });
options.Filters.Add(new MessageActionFilter() { Order = 1 });
options.Filters.Add<ResultFilter>();
}
}

前一步建立了擴充方法後便可以在 Startup 裡的 ConfigureServices 直接註冊使用

1
2
3
4
services.AddControllers(options =>
{
options.AddMessageFilter();
});

使用 Attribute 套用特定 Controller 或 Action

在 Controller 或 Action 上加上 [TypeFilter(type)] 就可以進行區域註冊

1
2
3
4
5
6
7
8
9
10
11
12
[Route("api/[controller]")]
[ApiController]
[TypeFilter(typeof(ValidationActionFilter))]
public class UserController : ControllerBase
{
[HttpGet("[action]")]
[TypeFilter(typeof(ValidationActionFilter))]
public async Task<UserViewModel> FindByUserIdAsync(int userId)
{
// ...
}
}

使用 [TypeFilter(type)] 有點不直覺也太長了,可以直接在 Filter 繼承 Attribute

1
2
3
4
5
6
7
public class ValidationActionFilter : IAsyncActionFilter, Attribute
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// ...
}
}

[Attribute] 註冊就可以改成直接填入Filter名稱

1
2
3
4
5
6
7
8
9
10
11
12
[Route("api/[controller]")]
[ApiController]
[ValidationActionFilter]
public class UserController : ControllerBase
{
[HttpGet("[action]")]
[ValidationActionFilter]
public async Task<UserViewModel> FindByUserIdAsync(int userId)
{
// ...
}
}

參考

[1]Filters in ASP.NET Core
[2]Filters 過濾器
[3]ASP.NET Core 2 系列 - Filters
[4]使用 Filter 統一 API 的回傳格式和例外處理
[5]Exploring Middleware as MVC Filters in ASP.NET Core 1.1
[6]Middleware vs Filters ASP. NET Core
[7]Filters in ASP.NET Core MVC