MasterNeverDown.Aspect 1.1.0

dotnet add package MasterNeverDown.Aspect --version 1.1.0
                    
NuGet\Install-Package MasterNeverDown.Aspect -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MasterNeverDown.Aspect" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MasterNeverDown.Aspect" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="MasterNeverDown.Aspect" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MasterNeverDown.Aspect --version 1.1.0
                    
#r "nuget: MasterNeverDown.Aspect, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MasterNeverDown.Aspect@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MasterNeverDown.Aspect&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=MasterNeverDown.Aspect&version=1.1.0
                    
Install as a Cake Tool

Vibe.Aspect

轻量级 .NET AOP 库 —— 给方法加 [LogAspect] / [RetryAspect] 特性,即可自动获得**调用日志(方法名/入参/返回值/耗时/异常)异常重试(多种退避策略)**能力。

NuGet Downloads License .NET

基于 Castle.DynamicProxy 运行时动态代理实现;对接 Microsoft.Extensions.Logging.ILogger,未注入日志时自动降级到带颜色的 Console 输出。


目录


特性

  • 零侵入:业务方法只需加 [LogAspect] / [RetryAspect] 特性
  • 两类切面:日志记录 + 异常重试,可独立或组合使用
  • 类级或方法级:可标注在类上(覆盖全部方法)或单个方法上(方法级优先)
  • 全异步支持Task / Task<T> / ValueTask / ValueTask<T> 都正确等待完成后再处理
  • 多种退避策略:固定 / 线性 / 指数 / 指数+抖动 四种重试退避
  • 异常过滤RetryOn/AbortOn 白/黑名单,按异常类型决定是否重试
  • DI 友好services.AddAspect<IFoo, Foo>() 一行接入 IServiceCollection
  • 可降级:未注册 ILoggerFactory 时自动用 Console 兜底输出
  • 复杂对象:使用 System.Text.Json 序列化,处理循环引用安全;超长截断
  • 零反射热路径:特性解析按 MethodInfo 缓存

安装

dotnet add package Vibe.Aspect

或在 csproj 中添加:

<PackageReference Include="Vibe.Aspect" Version="1.0.0" />

前置要求:.NET 10 或更高版本


快速开始

using Vibe.Aspect;

public interface ICalculator
{
    int Add(int a, int b);
}

public class Calculator : ICalculator
{
    [LogAspect]
    public int Add(int a, int b) => a + b;
}

// 手动代理(无 DI)
var calc = AspectProxyFactory.CreateInterfaceProxy<ICalculator>(new Calculator());
calc.Add(3, 4);

// 控制台输出:
// [Aspect:Enter] Calculator.Add(a=3, b=4)
// [Aspect:Exit ] Calculator.Add elapsed=0.265ms => 7

三种使用方式

方式 1:手动代理(无 DI)

适合控制台/脚本/单元测试等无 DI 容器的场景。

using Vibe.Aspect;

var calc = AspectProxyFactory.CreateInterfaceProxy<ICalculator>(new Calculator());
calc.Add(1, 2);

// 也支持类代理(方法需 virtual)
var svc = AspectProxyFactory.CreateClassProxy<OrderService>();

方式 2:DI + 接口代理(推荐)

最常用、最干净的方式。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Vibe.Aspect.DependencyInjection;

var services = new ServiceCollection();
services.AddLogging(b => b.AddSimpleConsole());

// 一行:把 ICalculator 注册为带切面的代理
services.AddAspect<ICalculator, Calculator>();

await using var sp = services.BuildServiceProvider();
var calc = sp.GetRequiredService<ICalculator>();
calc.Add(1, 2); // 自动通过 ILogger 输出

可指定生命周期:

services.AddAspect<ICalculator, Calculator>(ServiceLifetime.Singleton);

方式 3:DI + 类代理(类级特性)

适合没有接口的服务类,方法必须是 virtual

[LogAspect] // 类级:所有 virtual 方法都被记录
public class OrderService
{
    public virtual async ValueTask<Order> CreateAsync(CreateOrderRequest req)
    {
        await Task.Delay(15);
        return new Order(Random.Shared.Next(), req.Product, req.Quantity * req.UnitPrice);
    }

    // 没有 virtual:不会被拦截,但运行正常
    public string GetVersion() => "v1.0";
}

services.AddAspectClass<OrderService>();

异步支持

切面对所有异步返回类型都做了正确等待,保证耗时和返回值在 Task 真正完成后才被记录

返回类型 行为
void 同步记录
T (同步) 同步记录入参/返回值/耗时
Task await 后记录耗时;无返回值
Task<T> await 后记录耗时和 T
ValueTask await 后记录耗时
ValueTask<T> await 后记录耗时和 T

异步异常同样被正确捕获:

[LogAspect]
public async Task<int> DivideAsync(int a, int b)
{
    await Task.Delay(10);
    if (b == 0) throw new ArgumentException(nameof(b));
    return a / b;
}

// 输出:
// [Aspect:Enter] Calculator.DivideAsync(a=10, b=0)
// [Aspect:Throw] Calculator.DivideAsync elapsed=10.234ms threw System.ArgumentException

LogAspectAttribute 参数详解

[LogAspect(
    LogParameters    = true,                  // 是否记录入参
    LogReturnValue   = true,                  // 是否记录返回值
    LogExecutionTime = true,                  // 是否记录耗时
    LogException     = true,                  // 是否记录异常
    Level            = LogLevel.Information,  // 正常日志级别
    ExceptionLevel   = LogLevel.Error,        // 异常日志级别
    MaxArgumentLength = 1024                  // 单参数最大字符长度(超出截断)
)]
public Order Find(int id) { ... }
参数 类型 默认值 说明
LogParameters bool true 入参
LogReturnValue bool true 返回值(void / Task 自动忽略)
LogExecutionTime bool true 执行耗时(毫秒,3 位小数)
LogException bool true 异常类型与堆栈
Level LogLevel Information Enter / Exit 日志级别
ExceptionLevel LogLevel Error 抛异常时的日志级别
MaxArgumentLength int 1024 单个参数序列化后最大字符数,超出截断并追加 ...<+N chars><= 0 表示不截断

优先级:方法级特性 > 类级特性。即类上写了 [LogAspect],方法上又写一个 [LogAspect(LogParameters=false)],方法上的设置优先。


RetryAspect 重试切面

给方法加 [RetryAspect] 即可在抛出异常时按配置自动重试。所有方式(手动代理、DI 接口代理、DI 类代理)默认都已挂载 Retry 拦截器,不需要任何额外注册——业务方法只关心特性配置。

快速示例

public interface IApiClient
{
    Task<string> FetchAsync(string url);
}

public class ApiClient : IApiClient
{
    [RetryAspect(
        MaxAttempts = 4,                                  // 共 1 次正常 + 3 次重试
        DelayMilliseconds = 100,                           // 基础延迟 100ms
        BackoffStrategy = BackoffStrategy.ExponentialWithJitter,  // 100 → 200 → 400 → ... + 抖动
        MaxDelayMilliseconds = 5000,                       // 单次延迟上限 5s
        RetryOn = new[] { typeof(IOException), typeof(TimeoutException) },  // 只对网络相关异常重试
        AbortOn = new[] { typeof(ArgumentException) }      // 客户端参数错误立即放弃
    )]
    public async Task<string> FetchAsync(string url)
    {
        // ... 你的业务逻辑
    }
}

// 注册:和 LogAspect 完全一样的注册方式
services.AddAspect<IApiClient, ApiClient>();

常见使用场景

场景 1:HTTP API 调用 —— 指数退避 + 抖动
public interface IHttpService
{
    Task<string> GetAsync(string url);
}

public class HttpService : IHttpService
{
    // 推荐配置:适用于调用外部 HTTP API
    [RetryAspect(
        MaxAttempts = 5,
        DelayMilliseconds = 200,
        BackoffStrategy = BackoffStrategy.ExponentialWithJitter,
        JitterFactor = 0.2,
        MaxDelayMilliseconds = 10000,  // 最多等待 10 秒
        RetryOn = new[] { typeof(HttpRequestException), typeof(TimeoutException) },
        AbortOn = new[] { typeof(UnauthorizedAccessException) }  // 401 不重试
    )]
    public async Task<string> GetAsync(string url)
    {
        using var client = new HttpClient();
        return await client.GetStringAsync(url);
    }
}

执行时序(假设前 4 次失败,第 5 次成功):

调用 1: 失败 → 等待 200ms
调用 2: 失败 → 等待 400ms ± 80ms
调用 3: 失败 → 等待 800ms ± 160ms
调用 4: 失败 → 等待 1600ms ± 320ms
调用 5: 成功
总耗时 ≈ 3.5 秒(含重试延迟)
场景 2:数据库操作 —— 固定延迟 + 仅瞬态错误
public interface IUserRepository
{
    Task<User> GetByIdAsync(int id);
    Task SaveAsync(User user);
}

public class UserRepository : IUserRepository
{
    // 读取:只重试连接超时等瞬态错误
    [RetryAspect(
        MaxAttempts = 3,
        DelayMilliseconds = 50,
        BackoffStrategy = BackoffStrategy.Fixed,
        RetryOn = new[] { typeof(SqlException) },
        AbortOn = new[] { typeof(DBConcurrencyException) }
    )]
    public async Task<User> GetByIdAsync(int id)
    {
        // SELECT 查询...
    }

    // 写入:禁用重试(写操作非幂等)
    [RetryAspect(MaxAttempts = 1)]  // 等价于禁用重试
    public async Task SaveAsync(User user)
    {
        // INSERT/UPDATE...
    }
}
场景 3:消息队列发送 —— 线性退避
public interface IMessagePublisher
{
    Task PublishAsync<T>(string topic, T message);
}

public class MessagePublisher : IMessagePublisher
{
    [RetryAspect(
        MaxAttempts = 4,
        DelayMilliseconds = 100,
        BackoffStrategy = BackoffStrategy.Linear,  // 100ms → 200ms → 300ms
        RetryOn = new[] { typeof(BrokerUnavailableException) }
    )]
    public async Task PublishAsync<T>(string topic, T message)
    {
        // 发送到 Kafka/RabbitMQ...
    }
}
场景 4:文件 IO —— 短延迟快速重试
public interface IFileService
{
    string ReadLockedFile(string path);
}

public class FileService : IFileService
{
    [RetryAspect(
        MaxAttempts = 10,
        DelayMilliseconds = 20,        // 短延迟
        BackoffStrategy = BackoffStrategy.Fixed,
        RetryOn = new[] { typeof(IOException) },
        LogRetries = false  // 频繁重试时可关闭日志减少噪音
    )]
    public string ReadLockedFile(string path)
    {
        return File.ReadAllText(path);  // 文件可能被其他进程短暂锁定
    }
}
场景 5:同步方法重试
public interface ICalculator
{
    int Divide(int a, int b);
}

public class Calculator : ICalculator
{
    [RetryAspect(
        MaxAttempts = 3,
        DelayMilliseconds = 100,
        BackoffStrategy = BackoffStrategy.Fixed
    )]
    public int Divide(int a, int b)
    {
        // 同步方法同样支持重试
        if (b == 0) throw new ArgumentException("除零");
        return a / b;
    }
}
场景 6:组合异常过滤 —— 白名单 + 黑名单
[RetryAspect(
    MaxAttempts = 5,
    DelayMilliseconds = 500,
    RetryOn = new[] {
        typeof(NetworkException),      // 网络错误:重试
        typeof(TimeoutException)       // 超时:重试
    },
    AbortOn = new[] {
        typeof(ValidationException),   // 参数错误:立即放弃
        typeof(AuthenticationException) // 认证失败:立即放弃
    }
)]
public async Task<OrderResult> SubmitOrderAsync(OrderRequest request)
{
    // RetryOn 和 AbortOn 同时设置时,AbortOn 优先级更高
    // 例如 AuthException 继承自 NetworkException,但命中 AbortOn 不会重试
}

退避策略 BackoffStrategy

策略 延迟公式 典型场景
Fixed delay = baseDelay 简单场景,固定间隔
Linear delay = baseDelay × attempt 渐进式退让
Exponential delay = baseDelay × 2^(attempt-1) 标准网络重试,快速放弃
ExponentialWithJitter 指数 ± JitterFactor 随机扰动 推荐用于分布式系统,避免雷霆群效应

所有策略都受 MaxDelayMilliseconds 上限约束。

RetryAspectAttribute 参数详解

参数 类型 默认值 说明
MaxAttempts int 3 总尝试次数(含首次)。≤ 1 等价于禁用重试
DelayMilliseconds int 100 基础延迟毫秒数。≤ 0 表示不等待立即重试
MaxDelayMilliseconds int 30000 单次延迟上限(避免指数退避无限增长)。≤ 0 不限制
BackoffStrategy BackoffStrategy Exponential 退避策略
JitterFactor double 0.2 抖动因子(0~1),仅对 ExponentialWithJitter 生效
RetryOn Type[]? null 白名单:只对这些异常类型(含子类)重试
AbortOn Type[]? null 黑名单:命中即立即放弃(优先级高于 RetryOn
LogRetries bool true 是否在每次重试时写日志
RetryLogLevel LogLevel Warning 重试中间日志级别

重试判定流程

方法抛异常
   │
   ├─ 是 OperationCanceledException? ──► 不重试,直接传播(取消语义优先)
   │
   ├─ AbortOn 命中? ───────────────────► 不重试,直接传播
   │
   ├─ RetryOn 设置了且未命中? ─────────► 不重试,直接传播
   │
   ├─ 已达 MaxAttempts? ───────────────► 写"已耗尽"日志,传播最后一次异常
   │
   └─ 重试 ─► 等待 GetDelay(attempt) ─► 重新调用方法

注意事项

  • OperationCanceledException 永远不会被重试——取消信号必须立即传播给调用方
  • 重试期间方法的副作用(写数据库、发消息等)会发生多次。只对幂等操作启用重试,或在方法内部加幂等保护
  • 异步方法用 Castle 的 IInvocationProceedInfo.CaptureProceedInfo() 安全重入,不会出现"跨 await 重入 Proceed" 的未定义行为

组合使用 LogAspect + RetryAspect

两个特性可以同时挂在同一个方法上。拦截链顺序是 Log(外)→ Retry(内)→ 目标,这意味着:

  • 每次"逻辑调用"只产生 1 对 [Aspect:Enter] / [Aspect:Exit] 日志(即使内部重试了多次)
  • 重试期间的每次尝试都会产生独立的 [Retry] 日志
  • 最终的耗时(elapsed=...)覆盖整个重试过程(含退避等待)
[LogAspect]                                            // 类级:所有方法都记调用日志
public class PaymentService : IPaymentService
{
    [RetryAspect(
        MaxAttempts = 3,
        BackoffStrategy = BackoffStrategy.ExponentialWithJitter,
        RetryOn = new[] { typeof(IOException), typeof(TimeoutException) },
        AbortOn = new[] { typeof(ArgumentException) })]   // 方法级:单独配置重试
    public virtual async Task<PaymentResult> ChargeAsync(int orderId)
    {
        // ...
    }
}

典型输出(前 2 次失败 + 第 3 次成功)

info: PaymentService[0] [Aspect:Enter] PaymentService.ChargeAsync(orderId=42)
warn: Vibe.Aspect.Retry[0] [Retry] IPaymentService.ChargeAsync attempt 1/3 failed: IOException: ...
warn: Vibe.Aspect.Retry[0] [Retry] IPaymentService.ChargeAsync attempt 2/3 failed: IOException: ...
info: Vibe.Aspect.Retry[0] [Retry] IPaymentService.ChargeAsync succeeded on attempt 3
info: PaymentService[0] [Aspect:Exit ] PaymentService.ChargeAsync elapsed=425.8ms => {"OrderId":42,"Status":"Paid"}


自定义日志器

实现 IAspectLogger 即可完全接管日志输出(写文件、推 APM、ES、Kafka 等):

using Vibe.Aspect.Abstractions;

public sealed class MyAspectLogger : IAspectLogger
{
    public void OnEnter(AspectInvocationContext ctx)
    {
        Console.WriteLine($">> {ctx.TypeName}.{ctx.MethodName} args={ctx.Arguments.Length}");
    }

    public void OnExit(AspectInvocationContext ctx)
    {
        Console.WriteLine($"<< {ctx.MethodName} {ctx.Elapsed.TotalMilliseconds}ms");
    }

    public void OnException(AspectInvocationContext ctx)
    {
        Console.WriteLine($"!! {ctx.MethodName} threw {ctx.Exception?.GetType().Name}");
    }
}

// 注册
services.AddVibeAspect<MyAspectLogger>();
services.AddAspect<ICalculator, Calculator>();

AspectInvocationContext 提供的元数据:

字段 类型 说明
Method MethodInfo 完整方法元信息
TypeName string 声明类型全名
MethodName string 方法名
Arguments object?[] 实参数组
Options LogAspectAttribute 当前特性配置
ReturnValue object? 返回值(OnExit 时填充)
Exception Exception? 异常对象(OnException 时填充)
Elapsed TimeSpan 执行耗时(OnExit/OnException 时填充)

输出示例

默认 Console(未注入 ILogger)

11:31:21.077 [Information] [Aspect:Enter] Calculator.Add(a=3, b=4)
11:31:21.091 [Information] [Aspect:Exit ] Calculator.Add elapsed=0.299ms => 7
11:31:21.117 [Information] [Aspect:Enter] Calculator.Divide(a=10, b=0)
11:31:21.117 [Error]       [Aspect:Throw] Calculator.Divide elapsed=0.342ms threw System.ArgumentException

接入 Microsoft.Extensions.Logging

11:31:21.196 info: Calculator[0] [Aspect:Enter] Calculator.Add(a=100, b=200)
11:31:21.199 info: Calculator[0] [Aspect:Exit ] Calculator.Add elapsed=0.106ms => 300

复杂对象(自动 JSON 序列化)

[Aspect:Enter] OrderService.CreateAsync(request={"Product":"Laptop","Quantity":2,"UnitPrice":8999})
[Aspect:Exit ] OrderService.CreateAsync elapsed=17.005ms => {"Id":7995,"Product":"Laptop","Total":17998}

注意事项与限制

这些是 Castle.DynamicProxy 运行时代理机制的本质约束,不是 Vibe.Aspect 的限制。

接口代理(推荐)

  • 方法只需定义在 public interface
  • 实现类可以是 internal / sealed,无影响
  • 每次方法调用走的是接口分发,类型必须从 DI/工厂取,不能 new Calculator() 直接拿

类代理

  • 方法必须是 public virtual
  • 类不能是 sealed
  • 非 virtual 方法不会被拦截(但可正常调用,无报错)
  • 类必须有可访问的构造函数

关于异常

  • 拦截器必须重新抛出业务异常(throw;)—— 这是 AOP 契约,切面只观测不吞异常,否则会破坏调用方逻辑
  • VS 调试器默认会在 first-chance exception 处暂停(即使被 catch),按 F5 继续即可,或用 Ctrl+F5 启动不调试

关于 DI 生命周期

  • IAspectLogger 注册为 Singleton
  • LogAspectInterceptor 注册为 Transient
  • 业务服务(AddAspect<TInterface,TImpl>)默认 Scoped,可通过参数指定

性能

措施
特性解析 ConcurrentDictionary<MethodInfo, LogAspectAttribute?> 缓存,热路径无反射
ProxyGenerator 全局单例复用(Castle 官方推荐),程序集生成结果有内部缓存
参数序列化 仅当 LogParameters=true 才触发;原生类型直接 ToString,复杂对象 System.Text.Json(预编译选项实例)
异步开销 每次调用增加 1 个 async state machine(包装 Task),不可避免

未标 [LogAspect] 的方法走 if attr is null { Proceed(); return; },开销 ≈ Castle 代理调用本身的开销(纳秒级)。


常见问题

Q:方法没被拦截?

  • A:检查是否走了代理实例(不是 new Service() 直接拿);类代理时确保方法是 public virtual

Q:能拦截 private / static 方法吗?

  • A:不能。Castle.DynamicProxy 只能拦截可被子类重写或接口分发的方法。这是 .NET 运行时的限制。

Q:能在 record / struct / sealed class 上用吗?

  • A:record class 默认是 sealed,需手动声明 public record class Foo 并加 virtual,或者改用接口代理。struct 不支持。sealed class 不能做类代理,需要接口代理。

Q:能不能给整个程序集加切面,不写特性?

  • A:可以实现自定义 IInterceptor 拦截所有方法,但本库默认聚焦"有 [LogAspect] 才记录"——零误伤、零意外开销。如果你想要"全局切面",直接用 Castle.DynamicProxy 配合自己的拦截器即可。

Q:调试时一直弹异常?

  • A:你看到的是 first-chance exception。Debug → Windows → Exception Settings → Common Language Runtime Exceptions 取消勾选,或者直接 Ctrl+F5 启动。

Q:性能能跑生产吗?

  • A:可以。Castle.DynamicProxy 是 Moq / Autofac / NSubstitute / Castle Windsor 等主流库的底座,已在大量生产服务运行多年。Vibe.Aspect 仅在其上加了一层带缓存的特性解析。

从源码构建

# 克隆并构建
git clone <repo-url>
cd Vibe.Aspect

# 构建整个解决方案
dotnet build Vibe.Aspect.slnx -c Release

# 运行示例
dotnet run -c Release --project tests/Vibe.Aspect.Sample

# 打包 NuGet
dotnet pack src/Vibe.Aspect -c Release -o artifacts

NuGet 源配置

仓库根目录的 NuGet.config<clear /> 隔离全局配置中可能失效的本地源,确保任何环境下都能从 nuget.org 还原依赖。如果你需要额外私有源,在该文件中追加即可。


项目结构

Vibe.Aspect/
├── NuGet.config                   # 隔离本地失效源
├── Vibe.Aspect.slnx               # 解决方案
├── README.md                      # 本文件(同时作为 NuGet 包 README)
├── src/Vibe.Aspect/               # 主库
│   ├── Vibe.Aspect.csproj
│   ├── Attributes/
│   │   └── LogAspectAttribute.cs
│   ├── Abstractions/
│   │   ├── AspectInvocationContext.cs
│   │   └── IAspectLogger.cs
│   ├── Internal/
│   │   └── ArgumentFormatter.cs   # JSON 序列化 + 截断
│   ├── Loggers/
│   │   └── DefaultAspectLogger.cs # ILogger 优先 + Console 兜底
│   ├── Interceptors/
│   │   └── LogAspectInterceptor.cs# 同/异步分派 + 缓存
│   ├── AspectProxyFactory.cs      # 静态代理工厂
│   └── DependencyInjection/
│       └── ServiceCollectionExtensions.cs
└── tests/Vibe.Aspect.Sample/      # 示例 / 验证项目
    ├── Program.cs
    ├── Calculator.cs              # 接口代理 demo
    └── OrderService.cs            # 类代理 demo

License

MIT © Vibe

依赖:

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 35 5/12/2026
1.0.2 35 5/12/2026
1.0.1 33 5/12/2026
1.0.0 37 5/12/2026