ZibStack.NET.Aop
AOP (Aspect-Oriented Programming) framework for .NET 8+ using C# interceptors. Define aspects that run before, after, or on exception of any method — at compile time, no runtime proxy or reflection.
See the working sample: SampleApi on GitHub
Compile-time diagnostics: every aspect placement in this guide is also validated by 15 Roslyn analyzers shipped in the same package —
AOP0001throughAOP0021. Bad placements ([Cache]on avoidmethod,[Retry(MaxAttempts = 0)],[Log]on aprivatemethod, …) light up in the IDE before you can build, and 7 of them have an Alt+Enter code fix. See AOP Analyzers — Compile-Time Diagnostics for the full reference.
Install
Section titled “Install”dotnet add package ZibStack.NET.AopThe package’s
build/.propsenablesInterceptorsNamespacesforZibStack.Generatedautomatically on restore — no manual.csprojedit required.
Setup (DI)
Section titled “Setup (DI)”All aspect handlers are resolved from DI. There are two things you must do at startup:
- Register every handler type in the DI container (
AddTransient/AddScoped/AddSingleton). Built-in handlers ship with a one-call helper:AddAop(). - Bridge the container to the aspect runtime by calling
UseAop()afterBuild().
using ZibStack.NET.Aop;
var builder = WebApplication.CreateBuilder(args);
// 1a. Register built-in ZibStack aspect handlers ([Trace], ...).builder.Services.AddAop();
// 1b. Register any of your own handlers that you reference via [AspectHandler(typeof(...))].builder.Services.AddTransient<TimingHandler>();builder.Services.AddSingleton<ITimingRecorder, MyMetricsRecorder>();
var app = builder.Build();
// 2. Bridge DI into the aspect runtime — one call, required once.app.Services.UseAop();Both steps are mandatory:
- Forget step 2 → first call into any aspect-decorated method throws:
InvalidOperationException: ZibStack.NET.Aop.AspectServiceProvider.ServiceProvider is not set. [Log] resolves ILogger<T> from DI; you must wire it once at app startup. For ASP.NET Core: 'var app = builder.Build(); app.Services.UseAop();' - Forget step 1 (handler missing from DI) → throws:
InvalidOperationException: Aspect handler 'YourHandler' is not registered in DI. Add 'builder.Services.AddTransient<YourHandler>();' at startup.
UseAop()is a thin wrapper that setsAspectServiceProvider.ServiceProvider = services. If you prefer the assignment form you can still use it — they are equivalent.
You’ll see the same error for every handler attribute you stack on a method, so register all of them up-front.
Dependency injection in handlers
Section titled “Dependency injection in handlers”Handlers are resolved from DI — they support constructor injection like any other service:
public class TimingHandler : IAspectHandler{ private readonly ILogger<TimingHandler> _logger; private readonly ITimingRecorder _recorder;
// Dependencies injected automatically by the DI container public TimingHandler(ILogger<TimingHandler> logger, ITimingRecorder recorder) { _logger = logger; _recorder = recorder; }
public void OnBefore(AspectContext ctx) { }
public void OnAfter(AspectContext ctx) { _logger.LogInformation("{Class}.{Method} completed in {Ms}ms", ctx.ClassName, ctx.MethodName, ctx.ElapsedMilliseconds); _recorder.Record(ctx.MethodName, ctx.ElapsedMilliseconds); }
public void OnException(AspectContext ctx, Exception ex) => _logger.LogWarning(ex, "{Class}.{Method} failed", ctx.ClassName, ctx.MethodName);}Fallback: If DI is not configured, the generator falls back to
new TimingHandler()— which requires a parameterless constructor. To use injected dependencies, always setAspectServiceProvider.ServiceProvider.
Project-wide defaults (fluent IAopConfigurator)
Section titled “Project-wide defaults (fluent IAopConfigurator)”Set defaults for built-in aspects in one place — explicit attribute arguments always win, so per-method [Retry(MaxAttempts = 5)] overrides a project default of 10.
public sealed class AopConfig : IAopConfigurator{ public void Configure(IAopBuilder b) { b.Retry(r => { r.MaxAttempts = 5; r.DelayMs = 200; }); b.Timeout(t => t.TimeoutMs = 10_000); b.Trace(t => t.IncludeParameters = false); b.Cache(c => c.DurationSeconds = 600); b.Metrics(m => m.MeterName = "checkout.aop"); }}One class per project, discovered automatically by the source generator. The Configure method body is parsed at compile time (Roslyn constant evaluation) — never invoked at runtime, so every right-hand-side must be a literal, constant, or enum member. Covers [Retry], [Timeout], [Trace], [Cache], [Metrics]; Polly + HybridCache extension packages retain their own attribute args.
Benchmarks
Section titled “Benchmarks”Runtime handler overhead per call, measured with BenchmarkDotNet on .NET 10.0:
| Method | Mean | Allocated |
|---|---|---|
| Direct call (no AOP) | 0.2 ns | 0 B |
| No params (zero-alloc) | 17.4 ns | 104 B |
| 1 runtime handler | 73.7 ns | 360 B |
| 2 stacked handlers | 106.0 ns | 672 B |
~74ns + 360B per handler per call. For typical API endpoints (1-10ms), this is <0.01% overhead.
For hot paths, use an inline emitter ([Log] does this) — see Inline Emitters vs Runtime Handlers.
Read more
Section titled “Read more”- Built-in aspects —
[Trace],[Retry],[Cache],[Metrics],[Timeout],[Authorize],[Validate],[Transaction]+ Polly / HybridCache. - Custom aspects & internals — write your own handlers (sync / around / async), class-level + multi-aspect application,
AspectContextAPI, inline emitters. - AOP Analyzers — compile-time diagnostics — the diagnostic IDs and code fixes you’ll see in your IDE.
Requirements
Section titled “Requirements”- .NET 8.0 or later (uses C# interceptors)
The package’s build/.props enables InterceptorsNamespaces automatically on restore — no manual .csproj edit needed.
License
Section titled “License”MIT