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

酷墨大大_7631

酷墨大大_7631

2026-01-21

367人浏览

原创

C# 12 Interceptors 无法用于并发 AOP,因其仅支持编译期重写同步方法,不支持 async 方法及状态机;替代方案包括 Source Generators 手动插桩、Castle 代理(有限制)或组合式扩展方法封装。

c# c# 12 的 interceptors 和并发代码的aop实现

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 GetData(),就生成一个 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");
    }
}</int>
  • 必须显式调用 GetData_WithTimeout(),原方法不变 —— 这是关键约束,没有“透明拦截”
  • Generator 无法修改已有 async 方法体,只能新增;也不能自动替换调用点(那属于 Roslyn Analyzer + CodeFix 范畴)
  • 若需支持 ValueTask 或流式 await(如 IAsyncEnumerable),需额外判断返回类型并生成对应逻辑

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

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

Umbrel Proxy Manager
Umbrel Proxy Manager

高效管理与交互 Umbrel 代理服务,支持 Docker 容器。自动发现运行服务,将内部 Docker IP 映射为可访问的外部地址。

下载

典型错误现象: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>> 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");
        }
    }
}</task></t></t>

然后这样用:

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

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

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

proxy c#

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

2023.08.02

6999

3

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.08.29

3345

6

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

2025.08.29

4368

10

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

2025.08.29

2965

10

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

2023.10.19

3208

3

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

2025.10.17

5209

12

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2025.12.29

3849

9

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

2026.01.19

486

15

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

2023.07.18

6467

5

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Gradle 安全最佳实践
Gradle 安全最佳实践

共0课时 | 0人学习

Gradle Wrapper 使用与安全指南
Gradle Wrapper 使用与安全指南

共0课时 | 0人学习

Golangci-lint命令行使用演示
Golangci-lint命令行使用演示

共0课时 | 0人学习