接口 IResultFilter、IAsyncResultFilter 的簡介和用法示例(.net)
〇、IResultFilter、IAsyncResultFilter 接口簡介
IResultFilter 是 ASP.NET Core MVC 管道中一個非常重要的過濾器接口,它主要是在操作結果(IActionResult)被執(zhí)行之前和之后,來執(zhí)行自定義邏輯。
這里的“操作結果”指的是控制器動作方法返回的 IActionResult 實例,例如 ViewResult、JsonResult、RedirectResult、ContentResult 等。
接口定義:
#region 程序集 Microsoft.AspNetCore.Mvc.Abstractions, Version=3.1.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
// C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\3.1.10\ref\netcoreapp3.1\Microsoft.AspNetCore.Mvc.Abstractions.dll
#endregion
namespace Microsoft.AspNetCore.Mvc.Filters
{
// 摘要:一個過濾器,用于在操作完成后對結果的加工
public interface IResultFilter : IFilterMetadata
{
// 摘要:在操作結果【執(zhí)行前】調用
void OnResultExecuting(ResultExecutingContext context);
// 摘要:在操作結果【執(zhí)行后】調用
void OnResultExecuted(ResultExecutedContext context);
}
// 異步版本【推薦使用】
public interface IAsyncResultFilter : IFilterMetadata
{
Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next);
}
}
由于 IAsyncResultFilter 提供了更好的異步支持(避免了 async void 的坑),并且可以更靈活地控制執(zhí)行流程(通過調用 next()),通常建議實現 IAsyncResultFilter 而不是 IResultFilter。
IResultFilter 可以用于:
修改操作結果:在結果執(zhí)行前,可以檢查、修改甚至替換即將執(zhí)行的 IActionResult。
在結果執(zhí)行后執(zhí)行邏輯:在結果(如視圖渲染完成、JSON 序列化完成、重定向發(fā)生后)執(zhí)行完畢后,執(zhí)行一些清理、日志記錄或審計操作。
短路結果執(zhí)行:在 OnResultExecuting 中,通過設置 ResultExecutingContext.Result 并調用 ResultExecutingContext.Cancel = true,可以完全跳過原始結果的執(zhí)行,直接返回一個新的結果。
重要區(qū)別:不要將 IResultFilter 與 IActionFilter 混淆。
- IActionFilter 作用于控制器動作方法本身的執(zhí)行前后(OnActionExecuting, OnActionExecuted)。
- IResultFilter 作用于動作方法返回的結果(IActionResult)的執(zhí)行前后。
一、方法 void OnResultExecuting(ResultExecutingContext context):操作結果執(zhí)行前
1.1 簡介
調用時機:在框架準備執(zhí)行 IActionResult(例如,開始渲染視圖或序列化 JSON)之前立即調用。
// 執(zhí)行大概順序:
AuthorizationFilter
↓
ResourceFilter
↓
ActionFilter (OnActionExecuting)
↓
Controller Action Method Executes
↓
ActionFilter (OnActionExecuted)
↓
IResultFilter (OnResultExecuting) // ← 【在這里】
↓
IActionResult Executes (e.g., View renders, JSON serializes)
↓
IResultFilter (OnResultExecuted)
↓
ExceptionFilter
↓
ResourceFilter
此時 Action 已經執(zhí)行完畢,IActionResult 對象已經創(chuàng)建,但結果(如視圖、JSON 響應)尚未執(zhí)行或寫入響應流。此時可以修改或替換即將執(zhí)行的 IActionResult。
主要用途:
- 檢查/修改 IActionResult:通過 context.Result 獲取或設置即將執(zhí)行的結果。可以修改它的屬性,或者完全替換它。
- 短路執(zhí)行:這是 IResultFilter 最強大的功能之一。可以通過創(chuàng)建一個新的 IActionResult(如 ContentResult, JsonResult, RedirectResult 等),將其賦值給 context.Result,然后設置 context.Cancel = true,這將阻止原始結果的執(zhí)行,框架會立即執(zhí)行你提供的新結果。
- 執(zhí)行前置邏輯:如記錄日志、驗證某些條件、向 HttpContext.Response 添加響應頭等。
ResultExecutingContext 參數:
- context.Result: 表示即將被執(zhí)行的 IActionResult。
-
- 讀取它(例如,檢查返回的是 JsonResult 還是 ViewResult)。
- 修改它(例如,替換為另一個 IActionResult,如將 JsonResult 改為 ContentResult)。
- context.Cancel:通過設置 context.Result 為一個新結果并調用 context.Cancel = true,可以阻止原始結果的執(zhí)行,并立即返回你設置的結果。
- context.Controller:獲取當前控制器實例。
- context.HttpContext:獲取當前 HTTP 上下文,可用于訪問請求、響應、會話等。
- context.Canceled:指示執(zhí)行是否已被取消(通常由其他篩選器設置)。
- context.ActionDescriptor:提供關于當前執(zhí)行的動作的信息。
- context.ModelState:提供關于模型驗證狀態(tài)的信息。
1.2 簡單的示例:添加自定義響應頭
public class AddHeaderResultFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
// 在結果執(zhí)行前添加一個自定義響應頭
context.HttpContext.Response.Headers.Add("X-Custom-Header", "MyValue");
// 繼續(xù)執(zhí)行原始結果
}
public void OnResultExecuted(ResultExecutedContext context)
{
// 結果執(zhí)行后可以記錄日志
Console.WriteLine($"Result '{context.Result}' executed for {context.HttpContext.Request.Path}");
}
}
1.3 示例:短路結果執(zhí)行
public class MaintenanceModeResultFilter : IResultFilter
{
private readonly bool _isInMaintenanceMode;
public MaintenanceModeResultFilter(bool isInMaintenanceMode)
{
_isInMaintenanceMode = isInMaintenanceMode;
}
public void OnResultExecuting(ResultExecutingContext context)
{
if (_isInMaintenanceMode)
{
// 創(chuàng)建一個維護模式的響應結果
var maintenanceResult = new ContentResult
{
Content = "<h1>網站正在維護中,請稍后再試。</h1>",
ContentType = "text/html"
};
// 將新結果賦值給 context
context.Result = maintenanceResult;
// 取消原始結果的執(zhí)行
context.Cancel = true;
}
// 如果不在維護模式,不設置 Cancel,原始結果將繼續(xù)執(zhí)行
}
public void OnResultExecuted(ResultExecutedContext context)
{
// 如果被短路,這里仍然會執(zhí)行
if (context.Canceled)
{
Console.WriteLine("Result execution was canceled (Maintenance Mode).");
}
}
}
1.4 示例:記錄結果執(zhí)行時間
public class TimingResultFilter : IResultFilter
{
private const string StopwatchKey = "ResultExecutionStopwatch";
public void OnResultExecuting(ResultExecutingContext context)
{
// 開始計時
var stopwatch = Stopwatch.StartNew();
context.HttpContext.Items[StopwatchKey] = stopwatch;
}
public void OnResultExecuted(ResultExecutedContext context)
{
// 停止計時并記錄
if (context.HttpContext.Items[StopwatchKey] is Stopwatch stopwatch)
{
stopwatch.Stop();
Console.WriteLine($"Result '{context.Result.GetType().Name}' executed in {stopwatch.ElapsedMilliseconds}ms.");
}
}
}
二、方法 void OnResultExecuted(ResultExecutedContext context):操作結果執(zhí)行后
2.1 簡介
調用時機:在 IActionResult 已經執(zhí)行完畢之后調用。如果 OnResultExecuting 中發(fā)生了異常,或者執(zhí)行被短路(context.Cancel = true),這個方法仍然會執(zhí)行。
執(zhí)行順序,詳見本文章節(jié):1.1。
主要用途:
- 執(zhí)行后置邏輯:如記錄日志、審計、清理資源。
- 檢查執(zhí)行結果:通過 context.Result 可以查看最終執(zhí)行的是哪個結果(可能是原始結果,也可能是 OnResultExecuting 中設置的新結果)。
- 檢查異常:通過 context.Exception 可以檢查在結果執(zhí)行過程中是否發(fā)生了未處理的異常(如果 context.Exception 不為 null)。注意,如果異常被處理了(例如在過濾器中捕獲并設置了結果),Exception 可能為 null。
ResultExecutedContext 參數:
context.Result:獲取最終執(zhí)行的 IActionResult(在 OnResultExecuting 中可能已被修改)。
context.Exception:獲取在結果執(zhí)行過程中發(fā)生的未處理異常。如果為 null,表示沒有異常。
context.HttpContext, context.ActionDescriptor, context.ModelState:與 ResultExecutingContext 中的同名屬性作用相同。
context.Canceled:如果執(zhí)行在 OnResultExecuting 中被取消(context.Cancel = true),則此屬性為 true。
2.2 示例:記錄日志
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
// 實現 IResultFilter 接口
public class SimpleLoggingResultFilter : IResultFilter
{
private readonly ILogger<SimpleLoggingResultFilter> _logger;
private readonly Stopwatch _stopwatch; // 用于計算執(zhí)行時間
// 通過依賴注入獲取 ILogger
public SimpleLoggingResultFilter(ILogger<SimpleLoggingResultFilter> logger)
{
_logger = logger;
_stopwatch = new Stopwatch();
}
// IResultFilter.OnResultExecuting: 在 Result 執(zhí)行前調用
// 這里我們用它來啟動計時器
public void OnResultExecuting(ResultExecutingContext context)
{
_stopwatch.Restart(); // 重置并啟動計時器
_logger.LogDebug($"準備執(zhí)行結果: {context.HttpContext.Request.Path}");
}
// IResultFilter.OnResultExecuted: 在 Result 執(zhí)行后調用
// 這是記錄最終日志的主要方法
public void OnResultExecuted(ResultExecutedContext context)
{
_stopwatch.Stop(); // 停止計時器
var httpContext = context.HttpContext;
var request = httpContext.Request;
var response = httpContext.Response;
// 獲取狀態(tài)碼
int statusCode = response.StatusCode;
// 構建日志消息
var logMessage = $"請求完成 - " +
$"路徑: {request.Path}, " +
$"方法: {request.Method}, " +
$"狀態(tài)碼: {statusCode}, " +
$"耗時: {_stopwatch.ElapsedMilliseconds}ms";
// 根據狀態(tài)碼選擇日志級別
if (statusCode >= 500)
{
_logger.LogError(logMessage);
}
else if (statusCode >= 400)
{
_logger.LogWarning(logMessage);
}
else
{
_logger.LogInformation(logMessage);
}
// 如果 Result 執(zhí)行過程中發(fā)生了未處理的異常
if (context.Exception != null)
{
_logger.LogError(context.Exception, "Result 執(zhí)行中發(fā)生未處理異常");
}
// 注意: OnResultExecuted 之后,響應通常已經發(fā)送給客戶端
// 在這里修改 context.Result 或設置跳過 (context.Canceled = true) 通常無效或可能導致錯誤
// 因為響應流可能已經關閉。
}
}
三、方法 Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next):異步方式【推薦使用】
3.1 簡介
OnResultExecutionAsync 是 IAsyncResultFilter 接口的核心方法,它可以取代舊的同步 IResultFilter 接口,提供了更靈活、更強大的異步處理能力。
ResultExecutingContext context:這個 context 對象封裝了 IActionResult 即將執(zhí)行時的環(huán)境信息。
幾個常用參數:
context.Result: 這是即將被執(zhí)行的 IActionResult 實例(如 ViewResult, JsonResult, RedirectResult 等)。你可以讀取、修改這個屬性,甚至替換它。
context.HttpContext: 提供對當前 HTTP 請求和響應的完全訪問。這是你操作響應頭、讀取請求信息、訪問會話等的主要途徑。
context.Controller: 指向執(zhí)行該結果的控制器實例。
context.Canceled: 一個 bool 屬性。如果設置為 true,它會短路 (short-circuit) 后續(xù)的篩選器管道和 IActionResult 的執(zhí)行。注意:僅僅設置 context.Canceled = true 并不會自動產生響應;你通常需要同時設置 context.Result 來提供一個替代的響應。
ResultExecutionDelegate next:這是一個委托(delegate),本質上是一個 Func<Task<ResultExecutedContext>>。
調用 await next() 會繼續(xù)執(zhí)行篩選器管道,最終導致 IActionResult 被實際執(zhí)行(例如,視圖被渲染,JSON 被序列化)。
await next() 的返回值是一個 ResultExecutedContext 對象,它包含了 IActionResult 執(zhí)行完成后的狀態(tài)信息。
- 執(zhí)行流程與 next() 的關鍵作用
為什么說 OnResultExecutionAsync 的執(zhí)行流程是環(huán)繞式 (around) 的?
- 前處理 (Before):你的代碼首先執(zhí)行 await next() 之前的邏輯。這對應于舊的 OnResultExecuting 階段。
- 調用 next():await next() 調用是核心。
-
- 觸發(fā)剩余的 IResultFilter/IAsyncResultFilter 的 OnResultExecutionAsync 方法(如果存在)。
- 最終執(zhí)行 IActionResult.ExecuteResultAsync (或同步版本)。
- 返回一個 ResultExecutedContext。
- 后處理 (After):await next() 完成后,你的代碼繼續(xù)執(zhí)行 await next() 之后的邏輯。這對應于舊的 OnResultExecuted 階段。此時,IActionResult 已經執(zhí)行完畢。
next() 的強大之處在于:它可以精確控制代碼在結果執(zhí)行前和后的運行,而且開發(fā)者還可以選擇不調用 next(),從而完全阻止原始 IActionResult 的執(zhí)行。
- ResultExecutedContext(來自 await next())
當 await next() 完成后,可以得到一個 ResultExecutedContext。它的關鍵屬性包括:
context.Result: 執(zhí)行后的 IActionResult(可能在管道中被修改過)。
context.Exception: 如果在 IActionResult 執(zhí)行過程中或之后的篩選器中拋出了未處理的異常,這里會包含該異常。
context.ExceptionHandled: 一個 bool,表示異常是否已被某個篩選器標記為已處理。如果 context.Exception != null && !context.ExceptionHandled,說明有一個未處理的異常。
context.Canceled: 表示執(zhí)行是否被取消(通常由前面的篩選器設置)。
context.HttpContext: 執(zhí)行完成后的上下文。
3.2 示例一:添加自定義響應頭
如下,是最常見的用法之一。在結果執(zhí)行前設置響應頭,確保所有通過 MVC 返回的響應都包含這些安全或元數據頭。
using Microsoft.AspNetCore.Mvc.Filters;
using System.Threading.Tasks;
public class SecurityHeadersFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(
ResultExecutingContext context,
ResultExecutionDelegate next)
{
var response = context.HttpContext.Response;
// 添加安全相關的響應頭
response.Headers.Add("X-Content-Type-Options", "nosniff");
response.Headers.Add("X-Frame-Options", "DENY");
response.Headers.Add("X-XSS-Protection", "1; mode=block");
// 注意:Content-Security-Policy 非常復雜,這里只是簡單示例
response.Headers.Add("Content-Security-Policy", "default-src 'self'");
// 添加自定義頭
response.Headers.Add("X-Generated-By", "My ASP.NET Core App");
// 繼續(xù)執(zhí)行后續(xù)的篩選器和 IActionResult
await next();
}
}
3.3 示例二:響應結果包裝(API 版本化或統(tǒng)一格式)
如下,過濾器將所有 JsonResult 的響應體包裝在一個包含元數據(如成功標志、時間戳)的通用結構中。這對于構建 RESTful API 非常有用,可以提供一致的響應格式。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Threading.Tasks;
public class ApiResponseWrapperFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(
ResultExecutingContext context,
ResultExecutionDelegate next)
{
// 1. 檢查當前結果是否是 JsonResult (我們只包裝 JSON 響應)
if (context.Result is not JsonResult)
{
// 不是 JSON,直接繼續(xù)執(zhí)行
await next();
return;
}
// 2. 準備包裝對象
var originalResult = context.Result as JsonResult;
var wrappedResponse = new
{
Success = true, // 假設操作成功
Timestamp = DateTime.UtcNow,
Data = originalResult.Value, // 將原始數據放入包裝對象的 Data 屬性
// Version = "1.0" // 可以加入 API 版本信息
};
// 3. 替換 context.Result 為新的 JsonResult
context.Result = new JsonResult(wrappedResponse)
{
// 保持原始 JsonResult 的序列化設置 (如 JsonSerializerOptions)
SerializerSettings = (originalResult as JsonResult)?.SerializerSettings
};
// 4. 繼續(xù)執(zhí)行。現在執(zhí)行的是我們包裝后的 JsonResult
await next();
}
}
// 使用示例控制器
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
[HttpGet]
public IActionResult GetData()
{
// 返回的匿名對象會被 ApiResponseWrapperFilter 包裝
return Ok(new { Name = "John", Age = 30 });
// 最終響應體: { "Success": true, "Timestamp": "...", "Data": { "Name": "John", "Age": 30 } }
}
}
3.4 示例三:基于條件的短路 (Short-circuiting) - 簡單緩存
如下,過濾器演示了強大的短路能力。它在 IActionResult 執(zhí)行前檢查緩存,如果命中,則直接用緩存的結果替換 context.Result 并設置 context.Cancel = true,從而跳過昂貴的 IActionResult 執(zhí)行過程(如數據庫查詢、視圖渲染)。如果未命中,則執(zhí)行 next(),并在執(zhí)行成功后將結果存入緩存。
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Memory;
using System.Threading.Tasks;
public class SimpleCacheFilter : IAsyncResultFilter
{
private readonly IMemoryCache _cache;
public SimpleCacheFilter(IMemoryCache cache)
{
_cache = cache;
}
public async Task OnResultExecutionAsync(
ResultExecutingContext context,
ResultExecutionDelegate next)
{
// 1. 為當前請求生成一個緩存鍵 (簡化示例,實際中需要更健壯的鍵)
var cacheKey = $"Result_{context.HttpContext.Request.Path}";
// 2. 嘗試從緩存中獲取結果
if (_cache.TryGetValue(cacheKey, out object cachedResult))
{
// 3. 緩存命中!短路執(zhí)行
// 將緩存的結果設置為 context.Result
context.Result = cachedResult as IActionResult;
// 標記為已取消,阻止 next() 執(zhí)行
context.Cancel = true;
// 記錄命中日志
Console.WriteLine($"Cache HIT for {cacheKey}");
return; // 直接返回,不執(zhí)行 next()
}
// 4. 緩存未命中,繼續(xù)執(zhí)行原始的 IActionResult
// 注意:我們調用 next(),它會返回 ResultExecutedContext
var executedContext = await next();
// 5. 檢查執(zhí)行是否成功且沒有異常/取消
if (!executedContext.Canceled && executedContext.Exception == null)
{
// 6. 將執(zhí)行后的結果(注意是 context.Result,不是 executedContext.Result)存入緩存
// (假設我們信任 context.Result 在執(zhí)行后是有效的)
_cache.Set(cacheKey, context.Result, TimeSpan.FromMinutes(5));
Console.WriteLine($"Cache SET for {cacheKey}");
}
// 如果執(zhí)行被取消或有異常,通常不緩存
}
}
3.5 示例四:性能監(jiān)控(測量 IActionResult 執(zhí)行時間)
如下,過濾器精確測量了 IActionResult 本身(不包括動作方法執(zhí)行時間)的執(zhí)行耗時。這對于識別性能瓶頸非常有用。try/finally 確保即使發(fā)生異常也能記錄時間。
using Microsoft.AspNetCore.Mvc.Filters;
using System.Diagnostics;
using System.Threading.Tasks;
public class PerformanceMonitorFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(
ResultExecutingContext context,
ResultExecutionDelegate next)
{
var stopwatch = Stopwatch.StartNew();
try
{
// 執(zhí)行 IActionResult
await next();
}
finally
{
stopwatch.Stop();
var elapsedMs = stopwatch.ElapsedMilliseconds;
// 獲取請求信息
var requestPath = context.HttpContext.Request.Path;
var httpMethod = context.HttpContext.Request.Method;
var statusCode = context.HttpContext.Response.StatusCode;
// 記錄性能日志 (這里用 Console 代替實際的日志框架)
Console.WriteLine($"[{httpMethod}] {requestPath} -> Status: {statusCode}, " +
$"Result Execution Time: {elapsedMs}ms");
// 在實際應用中,這里可能會發(fā)送到 Application Insights, Prometheus, 或寫入日志文件
}
}
}
四、過濾器的注冊
注冊可以在三個地方實現,如下:
// 在 Program.cs 中注冊
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
options.Filters.Add<ApiResponseWrapperFilter>();
options.Filters.Add<AddHeaderFilter>();
});
// 在 Controller/Action 上使用特性
[ServiceFilter(typeof(ApiResponseWrapperFilter))]
public class HomeController : Controller
{
// ...
}
本文來自博客園,作者:橙子家,歡迎微信掃碼關注博主【橙子家czzj】,有任何疑問歡迎溝通,共同成長!

浙公網安備 33010602011771號