Skip to content

Commit 7e8cbe5

Browse files
committed
Merge feature/di-support: DI support for aspect handlers and Log emitter
2 parents 2a2f750 + 55b3389 commit 7e8cbe5

13 files changed

Lines changed: 334 additions & 81 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ Built-in aspects (no extra dependencies):
6666
public async Task<Order> GetOrderAsync(int id) { ... }
6767
// → Jaeger/Zipkin/OTLP see: OrderService.GetOrderAsync with params as tags
6868
69-
// Timing — lightweight metrics via event:
70-
TimingHandler.OnTimingRecorded += (cls, method, ms) => metrics.Record(method, ms);
69+
// Timing — lightweight metrics (DI or static event):
70+
// DI: builder.Services.AddSingleton<ITimingRecorder, MyRecorder>();
71+
// Static: TimingHandler.OnTimingRecorded += (cls, method, ms) => ...;
7172
[Timing]
7273
public Order PlaceOrder(int id) { ... }
7374

‎packages/ZibStack.NET.Aop/README.md‎

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,31 @@ Parameters are added as span tags. Exceptions set error status + add exception e
3535

3636
### [Timing] — lightweight metrics
3737

38-
Records method execution time via a static event. No external dependencies.
38+
Records method execution time. Supports DI with `ITimingRecorder` or static event fallback.
3939

4040
```csharp
41-
// Subscribe to timing events:
41+
// Option 1 — DI (recommended):
42+
builder.Services.AddSingleton<ITimingRecorder, MyMetricsRecorder>();
43+
builder.Services.AddTransient<TimingHandler>();
44+
45+
// Option 2 — static event (no DI needed):
4246
TimingHandler.OnTimingRecorded += (className, methodName, elapsedMs) =>
43-
{
4447
Console.WriteLine($"{className}.{methodName}: {elapsedMs}ms");
45-
// or: myMetrics.RecordHistogram("method_duration", elapsedMs, methodName);
46-
};
4748

4849
[Timing]
4950
public Order GetOrder(int id) { ... }
5051
```
5152

5253
### [SuppressException] — exception observability
5354

54-
Fires an event when a method throws. Useful for recording exceptions without changing control flow.
55+
Fires when a method throws. Supports DI with `IExceptionObserver` or static event fallback.
5556

5657
```csharp
58+
// Option 1 — DI:
59+
builder.Services.AddSingleton<IExceptionObserver, MyExceptionLogger>();
60+
builder.Services.AddTransient<SuppressExceptionHandler>();
61+
62+
// Option 2 — static event:
5763
SuppressExceptionHandler.OnExceptionSuppressed += (ctx, ex) =>
5864
logger.LogWarning(ex, "Exception in {Method}", ctx.MethodName);
5965

@@ -72,23 +78,34 @@ public Order GetOrder(int id) { ... }
7278
public async Task<Order> GetOrderAsync(int id) { ... }
7379
```
7480

75-
### [Cache] — in-memory caching
81+
### [Cache] — caching
82+
83+
Supports DI with `IAspectCache` (Redis, IMemoryCache, etc.) or built-in in-memory fallback.
7684

7785
```csharp
86+
// Option 1 — DI with custom cache:
87+
builder.Services.AddSingleton<IAspectCache, RedisAspectCache>();
88+
builder.Services.AddTransient<CacheHandler>();
89+
90+
// Option 2 — built-in in-memory (no DI needed):
7891
[Cache(DurationSeconds = 60)]
7992
public Order GetOrder(int id) { ... }
80-
// First call: executes, caches result
81-
// Same params again: returns cached (no execution)
8293

83-
// Invalidate:
94+
// Invalidate (fallback cache only):
8495
CacheHandler.Invalidate("GetOrder");
8596
CacheHandler.ClearAll();
8697
```
8798

8899
### [Authorize] — authorization check
89100

101+
Supports DI with `IAuthorizationChecker` or static delegate fallback.
102+
90103
```csharp
91-
// Setup once at startup:
104+
// Option 1 — DI (recommended):
105+
builder.Services.AddScoped<IAuthorizationChecker, MyAuthChecker>();
106+
builder.Services.AddTransient<AuthorizeHandler>();
107+
108+
// Option 2 — static delegate:
92109
AuthorizeHandler.AuthorizationCheck = (ctx, policy) =>
93110
currentUser.HasPermission(policy ?? ctx.MethodName);
94111

@@ -99,6 +116,53 @@ public void DeleteOrder(int id) { ... } // checks "DeleteOrder" permission
99116
public void PurgeAllData() { ... } // checks "Admin" permission
100117
```
101118

119+
## Dependency Injection
120+
121+
All aspect handlers support DI. Register handlers and their dependencies in your DI container, then wire up `AspectServiceProvider`:
122+
123+
```csharp
124+
var builder = WebApplication.CreateBuilder(args);
125+
126+
// Register handler dependencies
127+
builder.Services.AddSingleton<ITimingRecorder, PrometheusTimingRecorder>();
128+
builder.Services.AddScoped<IAuthorizationChecker, JwtAuthChecker>();
129+
builder.Services.AddSingleton<IAspectCache, RedisAspectCache>();
130+
131+
// Register handlers themselves
132+
builder.Services.AddTransient<TimingHandler>();
133+
builder.Services.AddTransient<AuthorizeHandler>();
134+
builder.Services.AddTransient<CacheHandler>();
135+
136+
var app = builder.Build();
137+
138+
// Enable DI for all aspect handlers (one line):
139+
AspectServiceProvider.ServiceProvider = app.Services;
140+
```
141+
142+
When `AspectServiceProvider.ServiceProvider` is set, generated code resolves handlers from DI. If DI is not configured or a handler is not registered, it falls back to `new` (backward compatible).
143+
144+
Custom handlers also benefit — just add constructor parameters:
145+
146+
```csharp
147+
public class MetricsHandler : IAspectHandler
148+
{
149+
private readonly ILogger<MetricsHandler> _logger;
150+
private readonly IMetrics _metrics;
151+
152+
public MetricsHandler(ILogger<MetricsHandler> logger, IMetrics metrics)
153+
{
154+
_logger = logger;
155+
_metrics = metrics;
156+
}
157+
158+
public void OnBefore(AspectContext ctx) { }
159+
public void OnAfter(AspectContext ctx)
160+
=> _metrics.RecordHistogram("method_duration", ctx.ElapsedMilliseconds, ctx.MethodName);
161+
public void OnException(AspectContext ctx, Exception ex)
162+
=> _logger.LogError(ex, "Failed {Method}", ctx.MethodName);
163+
}
164+
```
165+
102166
## Custom Aspects
103167

104168
### Sync handler (works on sync + async methods)
Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,21 @@
11
using ZibStack.NET.Aop;
2+
using ZibStack.NET.Aop.Aspects;
23

34
namespace ZibStack.NET.Aop.Sample.Aspects;
45

56
/// <summary>
6-
/// Simple timing aspect — logs elapsed time via IAspectHandler.
7+
/// Custom timing recorder that uses ILogger via DI.
8+
/// No more static events needed!
79
/// </summary>
8-
[AspectHandler(typeof(TimingHandler))]
9-
public class TimingAttribute : AspectAttribute
10+
public class LoggingTimingRecorder : ITimingRecorder
1011
{
11-
}
12+
private readonly ILogger<LoggingTimingRecorder> _logger;
1213

13-
public class TimingHandler : IAspectHandler
14-
{
15-
public void OnBefore(AspectContext context)
16-
{
17-
Console.WriteLine($"[Timing] Starting {context.ClassName}.{context.MethodName}({context.FormatParameters()})");
18-
}
19-
20-
public void OnAfter(AspectContext context)
21-
{
22-
Console.WriteLine($"[Timing] Completed {context.ClassName}.{context.MethodName} in {context.ElapsedMilliseconds}ms");
23-
}
14+
public LoggingTimingRecorder(ILogger<LoggingTimingRecorder> logger) => _logger = logger;
2415

25-
public void OnException(AspectContext context, Exception exception)
16+
public void Record(string className, string methodName, long elapsedMilliseconds)
2617
{
27-
Console.WriteLine($"[Timing] Failed {context.ClassName}.{context.MethodName} after {context.ElapsedMilliseconds}ms: {exception.Message}");
18+
_logger.LogInformation("[Timing] {Class}.{Method} completed in {Ms}ms",
19+
className, methodName, elapsedMilliseconds);
2820
}
2921
}

‎packages/ZibStack.NET.Aop/sample/SampleApi/Program.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1+
using ZibStack.NET.Aop;
2+
using ZibStack.NET.Aop.Aspects;
3+
using ZibStack.NET.Aop.Sample.Aspects;
14
using ZibStack.NET.Aop.Sample.Services;
25

36
var builder = WebApplication.CreateBuilder(args);
7+
8+
// Register services
49
builder.Services.AddSingleton<OrderService>();
10+
11+
// Register aspect handlers with DI — handlers now get constructor-injected dependencies
12+
builder.Services.AddSingleton<ITimingRecorder, LoggingTimingRecorder>();
13+
builder.Services.AddTransient<TimingHandler>();
14+
515
var app = builder.Build();
616

17+
// Wire up DI for all aspect handlers
18+
AspectServiceProvider.ServiceProvider = app.Services;
19+
720
app.MapGet("/order/{id:int}", (int id, OrderService service) =>
821
{
922
var order = service.GetOrder(id);

‎packages/ZibStack.NET.Aop/sample/SampleApi/Services/OrderService.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using ZibStack.NET.Aop.Sample.Aspects;
1+
using ZibStack.NET.Aop.Aspects;
22

33
namespace ZibStack.NET.Aop.Sample.Services;
44

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
3+
namespace ZibStack.NET.Aop;
4+
5+
/// <summary>
6+
/// Static service locator for resolving aspect handlers from DI.
7+
/// When configured, handlers are resolved from <see cref="ServiceProvider"/> instead of
8+
/// being instantiated with <c>new</c>. This allows handlers to receive constructor-injected
9+
/// dependencies (ILogger, IMemoryCache, IHttpContextAccessor, etc.).
10+
/// </summary>
11+
/// <example>
12+
/// <code>
13+
/// // Program.cs — register your handlers and wire up the provider:
14+
/// builder.Services.AddTransient&lt;TimingHandler&gt;();
15+
/// var app = builder.Build();
16+
/// AspectServiceProvider.ServiceProvider = app.Services;
17+
/// </code>
18+
/// </example>
19+
public static class AspectServiceProvider
20+
{
21+
/// <summary>
22+
/// The application's service provider. Set this at startup to enable DI for aspect handlers.
23+
/// When null (default), handlers are created with parameterless constructors (backward compatible).
24+
/// </summary>
25+
public static IServiceProvider? ServiceProvider { get; set; }
26+
27+
/// <summary>
28+
/// Resolves a handler from <see cref="ServiceProvider"/>, or returns null if DI is not configured
29+
/// or the type is not registered.
30+
/// </summary>
31+
public static T? Resolve<T>() where T : class
32+
{
33+
return ServiceProvider?.GetService(typeof(T)) as T;
34+
}
35+
}

‎packages/ZibStack.NET.Aop/src/ZibStack.NET.Aop.Abstractions/Aspects/AuthorizeAttribute.cs‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ namespace ZibStack.NET.Aop.Aspects;
22

33
/// <summary>
44
/// Checks authorization before method execution. Throws <see cref="UnauthorizedAccessException"/>
5-
/// if the check fails. Configure the authorization logic via <see cref="AuthorizeHandler.AuthorizationCheck"/>.
5+
/// if the check fails. Configure via DI (<see cref="IAuthorizationChecker"/>) or
6+
/// static <see cref="AuthorizeHandler.AuthorizationCheck"/>.
67
/// </summary>
78
/// <example>
89
/// <code>
9-
/// // Setup (once, at startup):
10-
/// AuthorizeHandler.AuthorizationCheck = (ctx, policy) =>
10+
/// // Option 1 — DI (recommended):
11+
/// builder.Services.AddScoped&lt;IAuthorizationChecker, MyAuthChecker&gt;();
12+
/// builder.Services.AddTransient&lt;AuthorizeHandler&gt;();
13+
///
14+
/// // Option 2 — static delegate:
15+
/// AuthorizeHandler.AuthorizationCheck = (ctx, policy) =&gt;
1116
/// {
1217
/// var user = GetCurrentUser();
1318
/// return user.HasPermission(policy ?? ctx.MethodName);
@@ -28,20 +33,37 @@ public sealed class AuthorizeAttribute : AspectAttribute
2833
public string? Policy { get; set; }
2934
}
3035

36+
/// <summary>
37+
/// Authorization checker interface. Register in DI to provide authorization logic.
38+
/// </summary>
39+
public interface IAuthorizationChecker
40+
{
41+
/// <summary>Return true to allow, false to deny.</summary>
42+
bool IsAuthorized(AspectContext context, string? policy);
43+
}
44+
3145
public sealed class AuthorizeHandler : IAroundAspectHandler
3246
{
33-
/// <summary>
34-
/// Set this at startup. Parameters: AspectContext, policy string (nullable).
35-
/// Return true to allow, false to deny.
36-
/// </summary>
47+
private readonly IAuthorizationChecker? _checker;
48+
49+
/// <summary>Static delegate fallback. Used when DI is not configured.</summary>
3750
public static Func<AspectContext, string?, bool>? AuthorizationCheck { get; set; }
3851

52+
public AuthorizeHandler() { }
53+
54+
public AuthorizeHandler(IAuthorizationChecker checker) => _checker = checker;
55+
3956
public object? Around(AspectContext context, Func<object?> proceed)
4057
{
4158
var policy = context.Properties.TryGetValue("Policy", out var p) && p is string s ? s : null;
4259

43-
var check = AuthorizationCheck;
44-
if (check != null && !check(context, policy))
60+
bool authorized;
61+
if (_checker != null)
62+
authorized = _checker.IsAuthorized(context, policy);
63+
else
64+
authorized = AuthorizationCheck?.Invoke(context, policy) ?? true;
65+
66+
if (!authorized)
4567
throw new UnauthorizedAccessException(
4668
$"Access denied to {context.ClassName}.{context.MethodName}" +
4769
(policy != null ? $" (policy: {policy})" : ""));

0 commit comments

Comments
 (0)