〇、IResultFilter、IAsyncResultFilter 接口简介
IResultFilter 是 ASP.NET Core MVC 管道中一个非常重要的过滤器接口,它主要是在操作结果(IActionResult)被执行之前和之后,来执行自定义逻辑。
这里的"操作结果"指的是控制器动作方法返回的 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
{
// 摘要:在操作结果【执行前】调用
void OnResultExecuting(ResultExecutingContext context);
// 摘要:在操作结果【执行后】调用
void OnResultExecuted(ResultExecutedContext context);
}
// 异步版本【推荐使用】
public interface IAsyncResultFilter : IFilterMetadata
{
Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next);
}
}
由于 IAsyncResultFilter 提供了更好的异步支持(避免了 async void 的坑),并且可以更灵活地控制执行流程(通过调用 next()),通常建议实现 IAsyncResultFilter 而不是 IResultFilter。
IResultFilter 可以用于:
修改操作结果: 在结果执行前,可以检查、修改甚至替换 即将执行的 IActionResult。
在结果执行后执行逻辑: 在结果(如视图渲染完成、JSON 序列化完成、重定向发生后)执行完毕后,执行一些清理、日志记录或审计 操作。
短路结果执行: 在 OnResultExecuting 中,通过设置 ResultExecutingContext.Result 并调用 ResultExecutingContext.Cancel = true,可以完全跳过原始结果的执行,直接返回一个新的结果。
重要区别:不要将 IResultFilter 与 IActionFilter混淆。
- IActionFilter 作用于控制器动作方法本身的执行前后(OnActionExecuting, OnActionExecuted)。
- IResultFilter 作用于动作方法返回的结果(IActionResult)的执行前后。
一、方法 void OnResultExecuting(ResultExecutingContext context):操作结果执行前
1.1 简介
调用时机:在框架准备执行 IActionResult (例如,开始渲染视图或序列化 JSON)之前立即调用。
// 执行大概顺序:
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 已经执行完毕,IActionResult 对象已经创建,但结果(如视图、JSON 响应)尚未执行或写入响应流。此时可以修改或替换即将执行的 IActionResult。
主要用途:
- **检查/修改 IActionResult:**通过 context.Result 获取或设置即将执行的结果。可以修改它的属性,或者完全替换它。
- **短路执行:**这是 IResultFilter 最强大的功能之一。可以通过创建一个新的 IActionResult(如 ContentResult, JsonResult, RedirectResult 等),将其赋值给 context.Result,然后设置 context.Cancel = true,这将阻止原始结果的执行,框架会立即执行你提供的新结果。
- **执行前置逻辑:**如记录日志、验证某些条件、向 HttpContext.Response 添加响应头等。
ResultExecutingContext 参数:
- context.Result: 表示即将被执行的 IActionResult。
-
- 读取它(例如,检查返回的是 JsonResult 还是 ViewResult)。
- 修改它(例如,替换为另一个 IActionResult,如将 JsonResult 改为 ContentResult)。
- context.Cancel:通过设置 context.Result 为一个新结果并调用 context.Cancel = true,可以阻止原始结果的执行,并立即返回你设置的结果。
- context.Controller:获取当前控制器实例。
- context.HttpContext:获取当前 HTTP 上下文,可用于访问请求、响应、会话等。
- context.Canceled:指示执行是否已被取消(通常由其他筛选器设置)。
- context.ActionDescriptor:提供关于当前执行的动作的信息。
- context.ModelState:提供关于模型验证状态的信息。
1.2 简单的示例:添加自定义响应头
public class AddHeaderResultFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
// 在结果执行前添加一个自定义响应头
context.HttpContext.Response.Headers.Add("X-Custom-Header", "MyValue");
// 继续执行原始结果
}
public void OnResultExecuted(ResultExecutedContext context)
{
// 结果执行后可以记录日志
Console.WriteLine($"Result '{context.Result}' executed for {context.HttpContext.Request.Path}");
}
}
1.3 示例:短路结果执行
public class MaintenanceModeResultFilter : IResultFilter
{
private readonly bool _isInMaintenanceMode;
public MaintenanceModeResultFilter(bool isInMaintenanceMode)
{
_isInMaintenanceMode = isInMaintenanceMode;
}
public void OnResultExecuting(ResultExecutingContext context)
{
if (_isInMaintenanceMode)
{
// 创建一个维护模式的响应结果
var maintenanceResult = new ContentResult
{
Content = "<h1>网站正在维护中,请稍后再试。</h1>",
ContentType = "text/html"
};
// 将新结果赋值给 context
context.Result = maintenanceResult;
// 取消原始结果的执行
context.Cancel = true;
}
// 如果不在维护模式,不设置 Cancel,原始结果将继续执行
}
public void OnResultExecuted(ResultExecutedContext context)
{
// 如果被短路,这里仍然会执行
if (context.Canceled)
{
Console.WriteLine("Result execution was canceled (Maintenance Mode).");
}
}
}
1.4 示例:记录结果执行时间
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):操作结果执行后
2.1 简介
调用时机:在 IActionResult 已经执行完毕之后调用。如果 OnResultExecuting 中发生了异常,或者执行被短路(context.Cancel = true),这个方法仍然会执行。
执行顺序,详见本文章节:1.1。
主要用途:
- **执行后置逻辑:**如记录日志、审计、清理资源。
- **检查执行结果:**通过 context.Result 可以查看最终执行的是哪个结果(可能是原始结果,也可能是 OnResultExecuting 中设置的新结果)。
- **检查异常:**通过 context.Exception 可以检查在结果执行过程中是否发生了未处理的异常(如果 context.Exception 不为 null)。注意,如果异常被处理了(例如在过滤器中捕获并设置了结果),Exception 可能为 null。
ResultExecutedContext 参数:
context.Result:获取最终执行的 IActionResult(在 OnResultExecuting 中可能已被修改)。
context.Exception:获取在结果执行过程中发生的未处理异常。如果为 null,表示没有异常。
context.HttpContext , context.ActionDescriptor , context.ModelState:与 ResultExecutingContext 中的同名属性作用相同。
context.Canceled:如果执行在 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; // 用于计算执行时间
// 通过依赖注入获取 ILogger
public SimpleLoggingResultFilter(ILogger<SimpleLoggingResultFilter> logger)
{
_logger = logger;
_stopwatch = new Stopwatch();
}
// IResultFilter.OnResultExecuting: 在 Result 执行前调用
// 这里我们用它来启动计时器
public void OnResultExecuting(ResultExecutingContext context)
{
_stopwatch.Restart(); // 重置并启动计时器
_logger.LogDebug($"准备执行结果: {context.HttpContext.Request.Path}");
}
// IResultFilter.OnResultExecuted: 在 Result 执行后调用
// 这是记录最终日志的主要方法
public void OnResultExecuted(ResultExecutedContext context)
{
_stopwatch.Stop(); // 停止计时器
var httpContext = context.HttpContext;
var request = httpContext.Request;
var response = httpContext.Response;
// 获取状态码
int statusCode = response.StatusCode;
// 构建日志消息
var logMessage = $"请求完成 - " +
$"路径: {request.Path}, " +
$"方法: {request.Method}, " +
$"状态码: {statusCode}, " +
$"耗时: {_stopwatch.ElapsedMilliseconds}ms";
// 根据状态码选择日志级别
if (statusCode >= 500)
{
_logger.LogError(logMessage);
}
else if (statusCode >= 400)
{
_logger.LogWarning(logMessage);
}
else
{
_logger.LogInformation(logMessage);
}
// 如果 Result 执行过程中发生了未处理的异常
if (context.Exception != null)
{
_logger.LogError(context.Exception, "Result 执行中发生未处理异常");
}
// 注意: OnResultExecuted 之后,响应通常已经发送给客户端
// 在这里修改 context.Result 或设置跳过 (context.Canceled = true) 通常无效或可能导致错误
// 因为响应流可能已经关闭。
}
}
三、方法 Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next):异步方式【推荐使用】
3.1 简介
OnResultExecutionAsync 是 IAsyncResultFilter 接口的核心方法,它可以取代旧的同步 IResultFilter 接口,提供了更灵活、更强大的异步处理能力。
**ResultExecutingContext context:**这个 context 对象封装了 IActionResult 即将执行时的环境信息。
几个常用参数:
context.Result : 这是即将被执行的 IActionResult 实例(如 ViewResult, JsonResult, RedirectResult 等)。你可以读取、修改这个属性,甚至替换它。
context.HttpContext : 提供对当前 HTTP 请求和响应的完全访问。这是你操作响应头、读取请求信息、访问会话等的主要途径。
context.Controller : 指向执行该结果的控制器实例。
context.Canceled: 一个 bool 属性。如果设置为 true,它会短路 (short-circuit) 后续的筛选器管道和 IActionResult 的执行。注意:仅仅设置 context.Canceled = true 并不会自动产生响应;你通常需要同时设置 context.Result 来提供一个替代的响应。
**ResultExecutionDelegate next:**这是一个委托(delegate),本质上是一个 Func<Task<ResultExecutedContext>>。
调用 await next() 会继续执行筛选器管道,最终导致 IActionResult 被实际执行(例如,视图被渲染,JSON 被序列化)。
await next() 的返回值是一个 ResultExecutedContext 对象,它包含了 IActionResult 执行完成后的状态信息。
- 执行流程与 next() 的关键作用
为什么说 OnResultExecutionAsync 的执行流程是环绕式 (around) 的?
- **前处理 (Before):**你的代码首先执行 await next() 之前的逻辑。这对应于旧的 OnResultExecuting 阶段。
- **调用 next():**await next() 调用是核心。
-
- 触发剩余的 IResultFilter/IAsyncResultFilter 的 OnResultExecutionAsync 方法(如果存在)。
- 最终执行 IActionResult.ExecuteResultAsync (或同步版本)。
- 返回一个 ResultExecutedContext。
- **后处理 (After):**await next() 完成后,你的代码继续执行 await next() 之后的逻辑。这对应于旧的 OnResultExecuted 阶段。此时,IActionResult 已经执行完毕。
next() 的强大之处在于:它可以精确控制代码在结果执行前和后的运行 ,而且开发者还可以选择不调用 next(),从而完全阻止原始 IActionResult 的执行。
- ResultExecutedContext(来自 await next())
当 await next() 完成后,可以得到一个 ResultExecutedContext。它的关键属性包括:
context.Result : 执行后的 IActionResult(可能在管道中被修改过)。
context.Exception : 如果在 IActionResult 执行过程中或之后的筛选器中抛出了未处理的异常,这里会包含该异常。
context.ExceptionHandled : 一个 bool,表示异常是否已被某个筛选器标记为已处理。如果 context.Exception != null && !context.ExceptionHandled,说明有一个未处理的异常。
context.Canceled : 表示执行是否被取消(通常由前面的筛选器设置)。
context.HttpContext: 执行完成后的上下文。
3.2 示例一:添加自定义响应头
如下,是最常见的用法之一。在结果执行前设置响应头,确保所有通过 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");
// 继续执行后续的筛选器和 IActionResult
await next();
}
}
3.3 示例二:响应结果包装(API 版本化或统一格式)
如下,过滤器将所有 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,直接继续执行
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. 继续执行。现在执行的是我们包装后的 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 执行前检查缓存,如果命中,则直接用缓存的结果替换 context.Result 并设置 context.Cancel = true,从而跳过昂贵的 IActionResult 执行过程(如数据库查询、视图渲染)。如果未命中,则执行 next(),并在执行成功后将结果存入缓存。
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. 缓存命中!短路执行
// 将缓存的结果设置为 context.Result
context.Result = cachedResult as IActionResult;
// 标记为已取消,阻止 next() 执行
context.Cancel = true;
// 记录命中日志
Console.WriteLine($"Cache HIT for {cacheKey}");
return; // 直接返回,不执行 next()
}
// 4. 缓存未命中,继续执行原始的 IActionResult
// 注意:我们调用 next(),它会返回 ResultExecutedContext
var executedContext = await next();
// 5. 检查执行是否成功且没有异常/取消
if (!executedContext.Canceled && executedContext.Exception == null)
{
// 6. 将执行后的结果(注意是 context.Result,不是 executedContext.Result)存入缓存
// (假设我们信任 context.Result 在执行后是有效的)
_cache.Set(cacheKey, context.Result, TimeSpan.FromMinutes(5));
Console.WriteLine($"Cache SET for {cacheKey}");
}
// 如果执行被取消或有异常,通常不缓存
}
}
3.5 示例四:性能监控(测量 IActionResult 执行时间)
如下,过滤器精确测量了 IActionResult 本身(不包括动作方法执行时间)的执行耗时。这对于识别性能瓶颈非常有用。try/finally 确保即使发生异常也能记录时间。
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
{
// 执行 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");
// 在实际应用中,这里可能会发送到 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
{
// ...
}