c# C# 12 的 Interceptors 和并发代码的AOP实现

来源:这里教程网 时间:2026-02-21 17:39:00 作者:

Interceptors 在 C# 12 中不能用于并发 AOP

C# 12 的

Interceptors
是编译期重写机制,不是运行时拦截,它无法作用于已编译的异步方法、
async
方法体、或任何涉及
Task
/
ValueTask
状态机的调用。你写一个
[InterceptsLocation(...)]
尝试拦截
DoWorkAsync()
,编译器会直接报错:
Interceptors cannot intercept async methods

这意味着:你不能靠 Interceptors 实现「在 await 前后自动加锁/记录耗时/注入取消检查」这类典型并发 AOP 场景。

Interceptor 只能重写普通同步方法、属性 getter/setter、构造函数调用点(且必须是源码可见、同一编译单元) 所有
await
表达式都由编译器展开为状态机类型(如
DoWorkAsync>d__5
),Interceptor 无法插入其中
即使你拦截了调用方的同步入口(比如
StartProcessing()
),也无法穿透到其内部的
await File.ReadAsync()

并发 AOP 的可行替代方案:Source Generators + 手动 await 插桩

如果你真需要对异步方法做横切逻辑(比如自动超时包装、上下文传播、重试策略),目前最可控的方式是用

Source Generator
在编译期生成带包装逻辑的新方法,并要求开发者显式调用生成的方法(而非原方法)。

例如:你定义一个

[AutoTimeout(3000)]
特性,Generator 检测到标记了该特性的
async Task<int> GetData()</int>
,就生成一个
GetData_WithTimeout()

public async Task<int> GetData_WithTimeout()
{
    using var cts = new CancellationTokenSource(3000);
    try
    {
        return await GetData().WaitAsync(cts.Token);
    }
    catch (OperationCanceledException) when (cts.IsCancellationRequested)
    {
        throw new TimeoutException("GetData timed out after 3000ms");
    }
}
必须显式调用
GetData_WithTimeout()
,原方法不变 —— 这是关键约束,没有“透明拦截”
Generator 无法修改已有 async 方法体,只能新增;也不能自动替换调用点(那属于 Roslyn Analyzer + CodeFix 范畴) 若需支持
ValueTask
或流式 await(如 IAsyncEnumerable),需额外判断返回类型并生成对应逻辑

运行时方案:AOP 框架仍依赖代理(如 Castle DynamicProxy)但有严重限制

在 .NET 6+ 上,

Castle.DynamicProxy
仍可对实现接口的类做异步方法代理,但它只拦截「接口调用」,且所有被代理的 async 方法必须声明为
Task
(不能是
ValueTask
),否则代理会丢失 await 上下文或引发
InvalidOperationException

典型错误现象:

System.InvalidOperationException: Synchronous operations are not permitted. Call WriteAsync or set AllowSynchronousIO to true.
—— 这往往是因为代理层同步等待了 async 方法,破坏了 ASP.NET Core 的 IO 上下文约束。

仅适用于 public 接口方法;sealed 类、private/protected async 方法完全不可代理 每次 await 都经过代理调度器,性能开销明显(尤其高频小任务) .NET 8+ 中
DispatchProxy
不支持 async 方法,
RealProxy
已废弃,实际只剩 Castle 可用但维护停滞

真正轻量且推荐的做法:组合式手动封装 + 命名约定

放弃“全自动 AOP”,改用显式但低侵入的封装模式。例如定义一组扩展方法,把横切逻辑收敛成可复用的组合子:

public static class ConcurrencyExtensions
{
    public static async Task<T> WithTimeout<T>(this Func<Task<T>> operation, int milliseconds, string opName = "")
    {
        using var cts = new CancellationTokenSource(milliseconds);
        try
        {
            return await operation().WaitAsync(cts.Token);
        }
        catch (OperationCanceledException) when (cts.IsCancellationRequested)
        {
            throw new TimeoutException($"Operation '{opName}' timed out");
        }
    }
}

然后这样用:

var result = await (() => repository.FetchDataAsync()).WithTimeout(5000, "FetchData");
无反射、无代理、无生成代码,纯委托 + async/await 组合,性能接近手写 调用点清晰可见,调试时堆栈干净,不会出现“Interceptor_xxx”或“GeneratedProxy”等干扰帧 配合 IDE 的 Live Template 或 ReSharper 宏,可以一键补全
.WithTimeout(...)
,效率不输 AOP

真正的难点不在语法或工具,而在于:你是否愿意让并发控制逻辑暴露在调用端 —— 这其实是更健康的职责划分。强行隐藏,反而会让 timeout、retry、cancellation 的语义变得模糊且难以追踪。

相关推荐