C# 12 Interceptors 无法用于并发 AOP,因其仅支持编译期重写同步方法,不支持 async 方法及状态机;替代方案包括 Source Generators 手动插桩、Castle 代理(有限制)或组合式扩展方法封装。
C# 12 的 Interceptors 是编译期重写机制,不是运行时拦截,它无法作用于已编译的异步方法、async 方法体、或任何涉及 Task/ValueTask 状态机的调用。你写一个 [InterceptsLocation(...)] 尝试拦截 DoWorkAsync(),编译器会直接报错:Interceptors cannot intercept async methods。
这意味着:你不能靠 Interceptors 实现「在 await 前后自动加锁/记录耗时/注入取消检查」这类典型并发 AOP 场景。
await 表达式都由编译器展开为状态机类型(如 DoWorkAsync>d
__5),Interceptor 无法插入其中StartProcessing()),也无法穿透到其内部的 await File.ReadAsync()
如果你真需要对异步方法做横切逻辑(比如自动超时包装、上下文传播、重试策略),目前最可控的方式是用 Source Generator 在编译期生成带包装逻辑的新方法,并要求开发者显式调用生成的方法(而非原方法)。
例如:你定义一个 [AutoTimeout(3000)] 特性,Generator 检测到标记了该特性的 async Task,就生成一个 GetData_WithTimeout():
public async TaskGetData_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(),原方法不变 —— 这是关键约束,没有“透明拦截”ValueTask 或流式 await(如 IAsyncEnumerable),需额外判断返回类型并生成对应逻辑在 .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 上下文约束。
DispatchProxy 不支持 async 方法,RealProxy 已废弃,实际只剩 Castle 可用但维护停滞放弃“全自动 AOP”,改用显式但低侵入的封装模式。例如定义一组扩展方法,把横切逻辑收敛成可复用的组合子:
public static class ConcurrencyExtensions
{
public static async Task WithTimeout(this Func> 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");
.WithTimeout(...),效率不输 AOP真正的难点不在语法或工具,而在于:你是否愿意让并发控制逻辑暴露在调用端 —— 这其实是更健康的职责划分。强行隐藏,反而会让 timeout、retry、cancellation 的语义变得模糊且难以追踪。