MasterNeverDown.Aspect
1.1.0
dotnet add package MasterNeverDown.Aspect --version 1.1.0
NuGet\Install-Package MasterNeverDown.Aspect -Version 1.1.0
<PackageReference Include="MasterNeverDown.Aspect" Version="1.1.0" />
<PackageVersion Include="MasterNeverDown.Aspect" Version="1.1.0" />
<PackageReference Include="MasterNeverDown.Aspect" />
paket add MasterNeverDown.Aspect --version 1.1.0
#r "nuget: MasterNeverDown.Aspect, 1.1.0"
#:package MasterNeverDown.Aspect@1.1.0
#addin nuget:?package=MasterNeverDown.Aspect&version=1.1.0
#tool nuget:?package=MasterNeverDown.Aspect&version=1.1.0
Vibe.Aspect
轻量级 .NET AOP 库 —— 给方法加
[LogAspect]/[RetryAspect]特性,即可自动获得**调用日志(方法名/入参/返回值/耗时/异常)与异常重试(多种退避策略)**能力。
基于 Castle.DynamicProxy 运行时动态代理实现;对接 Microsoft.Extensions.Logging.ILogger,未注入日志时自动降级到带颜色的 Console 输出。
目录
- 特性
- 安装
- 快速开始
- 三种使用方式
- 异步支持
- LogAspectAttribute 参数详解
- RetryAspect 重试切面 🆕
- 组合使用 LogAspect + RetryAspect 🆕
- 自定义日志器
- 输出示例
- 注意事项与限制
- 性能
- 常见问题
- 从源码构建
- 项目结构
- License
特性
- ✅ 零侵入:业务方法只需加
[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注册为 SingletonLogAspectInterceptor注册为 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
依赖:
- Castle.Core (Apache-2.0)
- Microsoft.Extensions.Logging.Abstractions (MIT)
- Microsoft.Extensions.DependencyInjection.Abstractions (MIT)
| Product | Versions 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. |
-
net10.0
- Castle.Core (>= 5.1.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.