Bulk Apply
Overview
Section titled “Overview”Instead of placing [Cache], [Retry], [Trace] on every method individually, use b.Apply<TAspect>() in your IAopConfigurator to apply aspects in bulk based on selectors:
public sealed class AopConfig : IAopConfigurator{ public void Configure(IAopBuilder b) { b.Apply<CacheAttribute>(to => to .Implementing<IRepository>() .PublicMethods() , c => c.DurationSeconds = 120); }}This is equivalent to placing [Cache(DurationSeconds = 120)] on every public method in every class that implements IRepository — but without touching any of those classes.
Selectors
Section titled “Selectors”All selectors are AND-combined (intersection). Chain as many as you need:
| Selector | Description |
|---|---|
.Namespace("X") | Classes whose namespace starts with X (e.g. MyApp.Services matches MyApp.Services.Orders) |
.Implementing<T>() | Classes implementing interface T |
.DerivedFrom<T>() | Classes inheriting from T |
.ClassesWhere(c => ...) | Filter by class metadata: c.Name, c.IsAbstract, c.IsSealed |
.MethodsWhere(m => ...) | Filter by method metadata: m.Name, m.IsAsync, m.IsPublic, m.IsStatic |
.PublicMethods() | Shortcut for .MethodsWhere(m => m.IsPublic) |
.Except<T>() | Exclude a specific class from matching |
Predicate expressions
Section titled “Predicate expressions”ClassesWhere and MethodsWhere accept lambda expressions parsed at compile time. Supported patterns:
// String predicatesc => c.Name.StartsWith("Order")c => c.Name.EndsWith("Service")c => c.Name.Contains("Payment")
// Boolean propertiesm => m.IsAsyncm => m.IsPublic && !m.IsStatic
// Combinedm => m.Name.StartsWith("Get") && m.IsAsyncConfiguring aspect properties
Section titled “Configuring aspect properties”The optional second lambda configures the aspect’s properties — same as named arguments on the attribute:
// Equivalent to [Retry(MaxAttempts = 5, DelayMs = 200)]b.Apply<RetryAttribute>(to => to .Namespace("MyApp.Services") .MethodsWhere(m => m.IsAsync), r => { r.MaxAttempts = 5; r.DelayMs = 200; });Examples
Section titled “Examples”Log all services globally (no [Log] on any class)
Section titled “Log all services globally (no [Log] on any class)”b.Apply<LogAttribute>(to => to .Namespace("MyApp.Services") .PublicMethods());Works through interfaces, generics, overloads, diamond inheritance, and DI dispatch.
Cache all repository methods
Section titled “Cache all repository methods”b.Apply<CacheAttribute>(to => to .Implementing<IRepository>() .PublicMethods(), c => c.DurationSeconds = 300);Retry all async service methods
Section titled “Retry all async service methods”b.Apply<RetryAttribute>(to => to .Namespace("MyApp.Services") .MethodsWhere(m => m.IsAsync), r => r.MaxAttempts = 3);Trace everything except health checks
Section titled “Trace everything except health checks”b.Apply<TraceAttribute>(to => to .DerivedFrom<BaseController>() .Except<HealthCheckController>());Metrics on all “Order” classes
Section titled “Metrics on all “Order” classes”b.Apply<MetricsAttribute>(to => to .ClassesWhere(c => c.Name.StartsWith("Order")));Timeout on all public async methods in the project
Section titled “Timeout on all public async methods in the project”b.Apply<TimeoutAttribute>(to => to .MethodsWhere(m => m.IsAsync && m.IsPublic), t => t.TimeoutMs = 5000);Interface dispatch (DI scenarios)
Section titled “Interface dispatch (DI scenarios)”Apply rules match classes, not interfaces. But calls through interfaces are intercepted automatically:
// DI registrationbuilder.Services.AddTransient<IOrderService, OrderService>();
// Apply rule — matches OrderService (the class)b.Apply<LogAttribute>(to => to.Namespace("MyApp.Services").PublicMethods());
// Call site through interface — intercepted ✓app.MapGet("/order/{id}", (int id, IOrderService svc) => svc.GetOrder(id));The generator sees that OrderService implements IOrderService and automatically generates an interface proxy interceptor. No extra configuration needed.
This works with:
- Simple interfaces —
IOrderService svc - Generic interfaces —
IRepo<Product> svc - Multiple implementations of the same interface (first impl wins for the proxy)
- Diamond inheritance —
IVersioned : INamed,ITaggable : INamed - Method overloads —
Execute(),Execute(int),Execute(string, int)
Explicit attribute on interface
Section titled “Explicit attribute on interface”You can also place aspects directly on an interface:
[Trace]public interface IOrderService{ Order GetOrder(int id);}This intercepts all calls through IOrderService regardless of which class implements it. If you ALSO have an Apply rule matching the impl class — deduplication kicks in, no conflict.
Priority
Section titled “Priority”- Explicit
[Attribute]on a method/class/interface always wins — Apply rules don’t override existing attributes - Apply rules add virtual aspects to methods that don’t already have them
- Project-wide defaults (
b.Retry(...),b.Cache(...)) fill in unset properties on both explicit attributes and Apply-applied aspects - Deduplication — if the same interface gets a model from both explicit attribute and Apply proxy, only one is kept (no duplicate interceptors)
How it works
Section titled “How it works”The generator parses the Configure method body at compile time — it is never invoked at runtime. Selector chains and predicate lambdas are evaluated against Roslyn symbols during source generation. Matched methods receive interceptors identical to those generated for explicit attributes.
For interface dispatch, the generator:
- Finds all classes matching the Apply rule
- For each class, discovers all source-declared interfaces it implements
- Generates an interface proxy (extension method on
this IMyInterface) so call sites through the interface hit the interceptor
Advanced scenarios
Section titled “Advanced scenarios”Generic interfaces with multiple type arguments
Section titled “Generic interfaces with multiple type arguments”public interface ICommandHandler<TCommand, TResult>{ TResult Handle(TCommand command);}
public class CreateOrderHandler : ICommandHandler<CreateOrder, OrderResult> { ... }public class CancelOrderHandler : ICommandHandler<CancelOrder, string> { ... }
// Both handlers intercepted — each closed generic gets its own proxyb.Apply<TraceAttribute>(to => to.Implementing<ICommandHandler<,>>().PublicMethods());// or simply:b.Apply<TraceAttribute>(to => to.Namespace("MyApp.Handlers").PublicMethods());Same interface implemented by many classes
Section titled “Same interface implemented by many classes”public interface ITransformFunction{ JsonNode Execute(JsonNode[] args);}
public class ConcatFunction : ITransformFunction { ... }public class UpperFunction : ITransformFunction { ... }public class TrimFunction : ITransformFunction { ... }// ... 20 more
// One rule instruments all of them — first impl generates the interface proxyb.Apply<LogAttribute>(to => to.Implementing<ITransformFunction>().PublicMethods());Composed interface (CQRS pattern)
Section titled “Composed interface (CQRS pattern)”public interface ICanHandle<TCommand> { string Handle(TCommand cmd); }public interface ICanHandle<TCommand, TResult> { TResult Handle(TCommand cmd); }
public interface IOrderHandler : ICanHandle<CreateOrder, OrderResult>, ICanHandle<CancelOrder>{ int GetPendingCount();}
public class OrderHandler : IOrderHandler { ... }
// All of these call sites are intercepted:IOrderHandler svc = ...;svc.GetPendingCount(); // own methodsvc.Handle(new CreateOrder(...)); // from ICanHandle<CreateOrder, OrderResult>
ICanHandle<CancelOrder> cancel = svc;cancel.Handle(new CancelOrder(42)); // from ICanHandle<CancelOrder>Mixed: concrete + interface call on same object
Section titled “Mixed: concrete + interface call on same object”var impl = new OrderService();IOrderService iface = impl;
impl.GetOrder(1); // intercepted via OrderService_Aop (concrete)iface.GetOrder(2); // intercepted via IOrderService_IfaceAop (proxy)// Both work, no conflict — different interceptor classesExcluding specific classes
Section titled “Excluding specific classes”b.Apply<LogAttribute>(to => to .Namespace("MyApp.Services") .PublicMethods() .Except<HealthCheckService>() // too noisy .Except<MetricsService>() // avoid recursion);Combining multiple Apply rules
Section titled “Combining multiple Apply rules”public sealed class AopConfig : IAopConfigurator{ public void Configure(IAopBuilder b) { // Observability on everything b.Apply<TraceAttribute>(to => to.Namespace("MyApp").PublicMethods()); b.Apply<LogAttribute>(to => to.Namespace("MyApp").PublicMethods());
// Retry only on external calls b.Apply<RetryAttribute>(to => to .Namespace("MyApp.External") .MethodsWhere(m => m.IsAsync) , r => r.MaxAttempts = 3);
// Cache on read-only repos b.Apply<CacheAttribute>(to => to .Implementing<IReadOnlyRepository>() .MethodsWhere(m => m.Name.StartsWith("Get")) , c => c.DurationSeconds = 60); }}A method can receive aspects from multiple rules — they stack in declaration order.