接口 IResultFilter、IAsyncResultFilter 的简介和用法示例(.net)

〇、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,可以完全跳过原始结果的执行,直接返回一个新的结果

重要区别:不要将 IResultFilterIActionFilter混淆。

  • 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
{
    // ...
}
相关推荐
bobz9656 小时前
Virtio-networking: 2019 总结 2020展望
后端
AntBlack6 小时前
每周学点 AI : 在 Modal 上面搭建一下大模型应用
后端
G探险者6 小时前
常见线程池的创建方式及应用场景
后端
bobz9656 小时前
virtio-networking 4: 介绍 vDPA 1
后端
柏油7 小时前
MySQL InnoDB 架构
数据库·后端·mysql
一个热爱生活的普通人8 小时前
Golang time 库深度解析:从入门到精通
后端·go
一只叫煤球的猫8 小时前
怎么这么多StringUtils——Apache、Spring、Hutool全面对比
java·后端·性能优化
MrHuang9658 小时前
保姆级教程 | 在Ubuntu上部署Claude Code Plan Mode全过程
后端
紫穹8 小时前
008.LangChain 输出解析器
后端