- The Decorator pattern for concerns — Each cross-cutting concern is implemented as a decorator that wraps the same interface it decorates:
csharp
public class LoggingOrderService : IOrderService
{
private readonly IOrderService _inner;
private readonly ILogger _logger;
public LoggingOrderService(IOrderService inner, ILogger logger)
{
_inner = inner;
_logger = logger;
}
public void PlaceOrder(Order order)
{
_logger.Log($"Placing order {order.Id}");
_inner.PlaceOrder(order);
_logger.Log($"Order {order.Id} placed");
}
}
The business logic class has zero knowledge of logging. The Composition Root stacks the decorator.
- Decorator stacking — Multiple concerns compose through nesting:
CachingService(LoggingService(AuthorizingService(RealService))). Each decorator handles one concern and delegates to the next. The order of stacking matters (authorization before caching, caching before logging) and is controlled explicitly in the Composition Root.
- Generic decorators for CQRS — When commands and queries implement generic interfaces like
ICommandHandler<T>, a single generic decorator can apply a concern to all commands:
csharp
public class AuditingCommandHandler<T> : ICommandHandler<T>
{
private readonly ICommandHandler<T> _inner;
public void Handle(T command) { / audit + delegate / }
}
This eliminates the need to write per-command decorators and scales to hundreds of handlers.
- Why not IL weaving or runtime proxies — IL weaving (PostSharp) modifies compiled code at build time, creating invisible behavior changes that don't appear in source code. Runtime proxies (Castle) generate dynamic subclasses, which can fail with sealed classes and add debugging complexity. Both approaches couple your codebase to specific tooling. Decorators are plain code — debuggable, testable, and framework-independent.
- The SOLID foundation — This approach only works when abstractions follow the Interface Segregation Principle (small, focused interfaces) and the Dependency Inversion Principle (depend on abstractions). Fat interfaces make decorators painful because every method must be implemented even if the concern only applies to one.