MasterNeverDown.Aspect
1.0.0
See the version list below for details.
dotnet add package MasterNeverDown.Aspect --version 1.0.0
NuGet\Install-Package MasterNeverDown.Aspect -Version 1.0.0
<PackageReference Include="MasterNeverDown.Aspect" Version="1.0.0" />
<PackageVersion Include="MasterNeverDown.Aspect" Version="1.0.0" />
<PackageReference Include="MasterNeverDown.Aspect" />
paket add MasterNeverDown.Aspect --version 1.0.0
#r "nuget: MasterNeverDown.Aspect, 1.0.0"
#:package MasterNeverDown.Aspect@1.0.0
#addin nuget:?package=MasterNeverDown.Aspect&version=1.0.0
#tool nuget:?package=MasterNeverDown.Aspect&version=1.0.0
Vibe.Aspect
轻量级 .NET AOP 库 —— 给方法加一个
[LogAspect]特性,即可自动记录方法名、入参、返回值、执行耗时、异常信息。
基于 Castle.DynamicProxy 运行时动态代理实现;对接 Microsoft.Extensions.Logging.ILogger,未注入日志时自动降级到带颜色的 Console 输出。
目录
特性
- ✅ 零侵入:业务方法只需加
[LogAspect]特性 - ✅ 类级或方法级:可标注在类上(覆盖全部方法)或单个方法上(方法级优先)
- ✅ 全异步支持:
Task/Task<T>/ValueTask/ValueTask<T>都会等待完成后再记录 - ✅ 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)],方法上的设置优先。
自定义日志器
实现 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.