🔍 Filters 與 Action 篩選器
📌 什麼是 Filter?
Filter(篩選器)就像機場安檢——在你的 Action 方法執行前後,自動幫你做一些額外的工作。
想像搭飛機的流程:
- Authorization Filter = 護照檢查(你有沒有資格搭這班飛機?)
- Resource Filter = 行李寄放櫃檯(可以快取,不用每次都重新打包)
- Action Filter = 安檢門(檢查你帶了什麼,出來時再檢查一次)
- Exception Filter = 緊急處理中心(出事了!在這裡統一處理)
- Result Filter = 登機門檢查(最後確認一切沒問題)
📋 Filter 的五種類型
請求進來
│
▼
┌─────────────────────┐
│ Authorization Filter │ ← 第 1 關:有沒有權限?
└──────────┬──────────┘
▼
┌─────────────────────┐
│ Resource Filter │ ← 第 2 關:快取?資源處理?
│ (OnExecuting) │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ Action Filter │ ← 第 3 關:Action 前檢查
│ (OnExecuting) │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ Action 方法執行 │ ← 你寫的程式碼在這裡執行
└──────────┬──────────┘
▼
┌─────────────────────┐
│ Action Filter │ ← 第 3 關:Action 後檢查
│ (OnExecuted) │
└──────────┬──────────┘
▼
┌─────────────────────┐
│ Result Filter │ ← 第 5 關:結果處理
└──────────┬──────────┘
▼
回傳結果
如果中間有 Exception Filter,任何一關出錯都會被它接住。
⚙️ IActionFilter 與 IAsyncActionFilter
// 同步版本:IActionFilter
public class LogActionFilter : IActionFilter // 實作 IActionFilter 介面
{
private readonly ILogger<LogActionFilter> _logger; // 注入日誌服務
public LogActionFilter(ILogger<LogActionFilter> logger) // 建構函式
{
_logger = logger; // 儲存日誌服務
}
// Action 執行「前」呼叫(像安檢門前的金屬探測器)
public void OnActionExecuting(ActionExecutingContext context)
{
var actionName = context.ActionDescriptor.DisplayName; // 取得 Action 名稱
_logger.LogInformation($"開始執行:{actionName}"); // 記錄開始時間
}
// Action 執行「後」呼叫(像離開安檢門時的檢查)
public void OnActionExecuted(ActionExecutedContext context)
{
var actionName = context.ActionDescriptor.DisplayName; // 取得 Action 名稱
if (context.Exception != null) // 如果有例外發生
{
_logger.LogError($"執行 {actionName} 時發生錯誤"); // 記錄錯誤
}
else
{
_logger.LogInformation($"完成執行:{actionName}"); // 記錄完成
}
}
}
// 非同步版本:IAsyncActionFilter(推薦使用)
public class AsyncLogFilter : IAsyncActionFilter // 實作非同步介面
{
private readonly ILogger<AsyncLogFilter> _logger; // 注入日誌服務
public AsyncLogFilter(ILogger<AsyncLogFilter> logger) // 建構函式
{
_logger = logger; // 儲存日誌服務
}
public async Task OnActionExecutionAsync(
ActionExecutingContext context, // 執行前的上下文
ActionExecutionDelegate next) // 代表下一步(執行 Action)
{
// === 在 Action 執行「前」做的事 ===
_logger.LogInformation("Action 即將執行"); // 記錄日誌
var stopwatch = System.Diagnostics.Stopwatch.StartNew(); // 開始計時
var resultContext = await next(); // 執行 Action(像按下開始鍵)
// === 在 Action 執行「後」做的事 ===
stopwatch.Stop(); // 停止計時
_logger.LogInformation($"Action 執行完成,花費 {stopwatch.ElapsedMilliseconds} ms");
}
}
🛠️ 實用範例:效能計時 Filter
// 計時 Filter:測量每個 API 端點的執行時間(像比賽用的計時器)
public class PerformanceFilter : IAsyncActionFilter // 非同步 Action Filter
{
private readonly ILogger<PerformanceFilter> _logger; // 日誌服務
public PerformanceFilter(ILogger<PerformanceFilter> logger) // 建構函式注入
{
_logger = logger; // 儲存日誌服務
}
public async Task OnActionExecutionAsync(
ActionExecutingContext context, // 執行前上下文
ActionExecutionDelegate next) // 下一步
{
var actionName = context.ActionDescriptor.DisplayName; // 取得 Action 名稱
var stopwatch = System.Diagnostics.Stopwatch.StartNew(); // 按下碼表
var result = await next(); // 執行 Action
stopwatch.Stop(); // 停止碼表
var elapsed = stopwatch.ElapsedMilliseconds; // 取得經過時間
if (elapsed > 500) // 如果超過 500 毫秒(太慢了!)
{
_logger.LogWarning($"⚠️ 慢速 Action:{actionName} 花了 {elapsed}ms"); // 警告
}
else
{
_logger.LogInformation($"✅ {actionName} 完成,{elapsed}ms"); // 正常記錄
}
}
}
🎯 Filter 的套用層級
// 層級 1:全域 Filter(所有 Controller 的所有 Action 都會經過)
// 在 Program.cs 中註冊
builder.Services.AddControllers(options =>
{
options.Filters.Add<PerformanceFilter>(); // 全域套用效能計時 Filter
options.Filters.Add(new RequireHttpsAttribute()); // 全域要求 HTTPS
});
// 層級 2:Controller 級 Filter(該 Controller 的所有 Action)
[ServiceFilter(typeof(LogActionFilter))] // 整個 Controller 套用日誌 Filter
public class ProductsController : Controller // 商品控制器
{
// 這裡的所有 Action 都會被 LogActionFilter 過濾
public IActionResult Index() // 清單頁面
{
return View(); // 回傳視圖
}
public IActionResult Details(int id) // 詳情頁面
{
return View(); // 回傳視圖
}
}
// 層級 3:Action 級 Filter(只影響單一 Action)
public class OrdersController : Controller // 訂單控制器
{
[ServiceFilter(typeof(PerformanceFilter))] // 只有這個 Action 套用計時 Filter
public IActionResult GetReport() // 取得報表(可能很慢,需要計時)
{
return View(); // 回傳視圖
}
public IActionResult Index() // 這個 Action 不會被 PerformanceFilter 過濾
{
return View(); // 回傳視圖
}
}
🔧 [ServiceFilter] 與 [TypeFilter]
// [ServiceFilter]:Filter 從 DI 容器取得(需要先在 Program.cs 註冊)
// 步驟 1:在 Program.cs 註冊 Filter
builder.Services.AddScoped<LogActionFilter>(); // 註冊到 DI 容器
// 步驟 2:用 [ServiceFilter] 使用
[ServiceFilter(typeof(LogActionFilter))] // 從 DI 容器取得 Filter 實例
public class MyController : Controller { } // 控制器
// [TypeFilter]:不需要先在 DI 中註冊,還能傳遞額外參數
[TypeFilter(typeof(CustomFilter), Arguments = new object[] { "特殊參數" })]
public class AnotherController : Controller { } // 可以傳參數的 Filter
// CustomFilter 可以接收建構函式參數
public class CustomFilter : IActionFilter // 自訂 Filter
{
private readonly string _prefix; // 前綴參數
private readonly ILogger<CustomFilter> _logger; // 注入的日誌服務
public CustomFilter(string prefix, ILogger<CustomFilter> logger)
{
_prefix = prefix; // 從 [TypeFilter] 的 Arguments 傳入
_logger = logger; // 從 DI 容器自動注入
}
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation($"[{_prefix}] Action 開始"); // 使用前綴記錄
}
public void OnActionExecuted(ActionExecutedContext context)
{
_logger.LogInformation($"[{_prefix}] Action 完成"); // 使用前綴記錄
}
}
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:搞錯 Filter 的執行順序
// ❌ 錯誤理解:以為 Filter 只有 "先進先出"
// 實際上 Filter 的執行順序像洋蔥——一層一層包裹,執行時先進後出
// 全域 Filter A (OnExecuting) → 進入
// Controller Filter B (OnExecuting) → 進入
// Action Filter C (OnExecuting) → 進入
// === Action 執行 ===
// Action Filter C (OnExecuted) → 離開
// Controller Filter B (OnExecuted) → 離開
// 全域 Filter A (OnExecuted) → 離開
// ✅ 正確理解:用 Order 屬性控制順序(數字越小越先執行)
builder.Services.AddControllers(options =>
{
options.Filters.Add<FilterA>(1); // Order=1,最先執行 OnExecuting,最後執行 OnExecuted
options.Filters.Add<FilterB>(2); // Order=2,第二個執行
options.Filters.Add<FilterC>(3); // Order=3,最後執行 OnExecuting,最先執行 OnExecuted
});
解釋: Filter 的執行順序像俄羅斯套娃——進去時從外到內(OnExecuting),出來時從內到外(OnExecuted)。如果你以為 A 先執行完才輪到 B,就會搞混執行順序。
❌ 錯誤 2:在 Filter 中產生副作用
// ❌ 錯誤寫法:在 Filter 中修改資料庫(Filter 應該做的是「檢查」而非「修改」)
public class BadFilter : IActionFilter // 不好的 Filter
{
private readonly MyDbContext _db; // 資料庫上下文
public BadFilter(MyDbContext db) { _db = db; } // 注入資料庫
public void OnActionExecuting(ActionExecutingContext context)
{
// 在 Filter 裡直接修改資料庫——這不是 Filter 該做的事!
_db.Logs.Add(new Log { Message = "有人來了" }); // 插入日誌
_db.SaveChanges(); // 儲存變更(如果這裡失敗會怎樣?)
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
// ✅ 正確寫法:Filter 只做輕量的檢查和記錄
public class GoodFilter : IActionFilter // 好的 Filter
{
private readonly ILogger<GoodFilter> _logger; // 只用日誌服務
public GoodFilter(ILogger<GoodFilter> logger) { _logger = logger; } // 注入日誌
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation("請求進入"); // 只記錄日誌,不修改資料庫
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
解釋: Filter 就像機場安檢——安檢員的工作是「檢查」而不是「幫你重新打包行李」。如果在 Filter 裡做太多事(例如修改資料庫),不但違反單一職責原則,還可能因為 Filter 的執行順序導致難以預測的問題。