MasterNeverDown.Aspect 1.0.0

There is a newer version of this package available.
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
                    
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.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MasterNeverDown.Aspect" Version="1.0.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.0.0
                    
#r "nuget: MasterNeverDown.Aspect, 1.0.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.0.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.0.0
                    
Install as a Cake Addin
#tool nuget:?package=MasterNeverDown.Aspect&version=1.0.0
                    
Install as a Cake Tool

Vibe.Aspect

轻量级 .NET AOP 库 —— 给方法加一个 [LogAspect] 特性,即可自动记录方法名、入参、返回值、执行耗时、异常信息

NuGet Downloads License .NET

基于 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 注册为 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