Class-Level Decorators¶
The [DecoratedBy] attribute is the primary way to apply decorators in DecoWeaver. Apply it to your service implementation classes to automatically wrap them with decorators.
Assembly-Level Alternative
For cross-cutting concerns applied to many implementations, consider using Assembly-Level Decorators instead. Class-level decorators are best for implementation-specific needs.
Basic Syntax¶
Generic Attribute¶
The most common and type-safe approach:
using DecoWeaver.Attributes;
[DecoratedBy<LoggingDecorator>]
public class UserRepository : IUserRepository
{
// Your implementation
}
Non-Generic Attribute¶
Alternative syntax using typeof():
using DecoWeaver.Attributes;
[DecoratedBy(typeof(LoggingDecorator))]
public class UserRepository : IUserRepository
{
// Your implementation
}
Both syntaxes are equivalent - use whichever you prefer.
Where to Apply¶
Apply [DecoratedBy] to implementation classes, not interfaces:
// ✅ Correct: Apply to implementation
[DecoratedBy<LoggingDecorator>]
public class UserRepository : IUserRepository { }
// ❌ Incorrect: Don't apply to interface
[DecoratedBy<LoggingDecorator>]
public interface IUserRepository { }
Decorator Requirements¶
For DecoWeaver to work correctly, your decorator must:
- Implement the same interface as the decorated class
- Accept the interface as a constructor parameter (typically first parameter)
- Have all dependencies resolvable from the DI container
public interface IUserRepository
{
Task<User> GetByIdAsync(int id);
}
// ✅ Valid decorator
public class LoggingUserRepository : IUserRepository
{
private readonly IUserRepository _inner; // 1. Same interface
private readonly ILogger _logger;
// 2. Accept interface in constructor
public LoggingUserRepository(IUserRepository inner, ILogger logger)
{
_inner = inner;
_logger = logger;
}
public async Task<User> GetByIdAsync(int id)
{
_logger.LogInformation("Getting user {Id}", id);
return await _inner.GetByIdAsync(id); // 3. Delegate to inner
}
}
// Apply the decorator
[DecoratedBy<LoggingUserRepository>]
public class UserRepository : IUserRepository
{
public async Task<User> GetByIdAsync(int id)
{
// Your implementation
}
}
Registration¶
Once you've applied the attribute, register your service normally:
var services = new ServiceCollection();
// DecoWeaver automatically applies the decorator
services.AddScoped<IUserRepository, UserRepository>();
// Or with other lifetimes
services.AddSingleton<IUserRepository, UserRepository>();
services.AddTransient<IUserRepository, UserRepository>();
DecoWeaver intercepts these registration calls and wraps your implementation with the decorator.
Factory Delegate Registration¶
New in v1.0.2-beta
Factory delegate support was added in version 1.0.2-beta.
DecoWeaver also supports factory delegate registrations, allowing you to use custom initialization logic while still applying decorators:
Two-Parameter Generic Factory¶
[DecoratedBy<CachingRepository>]
public class UserRepository : IUserRepository
{
// Your implementation
}
// Factory delegate with two type parameters
services.AddScoped<IUserRepository, UserRepository>(sp =>
new UserRepository());
Single-Parameter Generic Factory¶
[DecoratedBy<LoggingRepository>]
public class UserRepository : IUserRepository
{
// Your implementation
}
// Factory delegate with single type parameter
services.AddScoped<IUserRepository>(sp =>
new UserRepository());
Complex Dependencies¶
Factory delegates can resolve dependencies from the IServiceProvider:
[DecoratedBy<CachingRepository>]
public class UserRepository : IUserRepository
{
private readonly ILogger _logger;
private readonly IOptions<DatabaseOptions> _options;
public UserRepository(ILogger logger, IOptions<DatabaseOptions> options)
{
_logger = logger;
_options = options;
}
// Implementation
}
// Register with factory that resolves dependencies
services.AddScoped<IUserRepository, UserRepository>(sp =>
{
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<UserRepository>();
var options = sp.GetRequiredService<IOptions<DatabaseOptions>>();
return new UserRepository(logger, options);
});
How It Works¶
When using factory delegates:
- Your factory logic is preserved - The lambda you provide is captured and used
- Decorators are applied around the result - DecoWeaver wraps the factory's output
- All lifetimes are supported -
AddScoped,AddTransient,AddSingleton
The generated code: - Registers your factory as a keyed service - Creates an outer factory that calls your factory and applies decorators - Maintains the same dependency resolution behavior you defined
// What you write:
services.AddScoped<IUserRepository, UserRepository>(sp =>
new UserRepository(sp.GetRequiredService<ILogger>()));
// What happens (conceptually):
// 1. Your factory is registered as keyed service
// 2. Outer factory applies decorators:
var repo = /* your factory result */;
var cached = new CachingRepository(repo);
var logged = new LoggingRepository(cached);
// 3. Logged instance is returned
Factory Delegate Limitations¶
Factory delegates work with: - ✅ Generic registration methods: AddScoped<T1, T2>(factory), AddScoped<T>(factory) - ✅ Keyed service registrations with factory delegates (as of v1.0.3-beta) - ✅ All standard lifetimes: Scoped, Transient, Singleton - ✅ Complex dependency resolution from IServiceProvider - ✅ Multiple decorators with ordering
Not currently supported: - ❌ Instance registrations - ❌ Open generic registration with typeof() syntax
Keyed Service Registration¶
New in v1.0.3-beta
Keyed service support was added in version 1.0.3-beta.
DecoWeaver supports keyed service registrations (introduced in .NET 8+), allowing you to register multiple implementations of the same interface under different keys while applying decorators independently:
Basic Keyed Service¶
[DecoratedBy<LoggingRepository<>>]
public class SqlRepository<T> : IRepository<T>
{
// Your SQL implementation
}
// Register with a key
services.AddKeyedScoped<IRepository<User>, SqlRepository<User>>("sql");
// Resolve using the key
var repo = serviceProvider.GetRequiredKeyedService<IRepository<User>>("sql");
// Returns: LoggingRepository<User> wrapping SqlRepository<User>
Multiple Keys for Same Service¶
Register multiple implementations with different keys - each gets decorated independently:
[DecoratedBy<CachingRepository<>>]
public class SqlRepository<T> : IRepository<T> { }
[DecoratedBy<CachingRepository<>>]
public class CosmosRepository<T> : IRepository<T> { }
// Register both with different keys
services.AddKeyedScoped<IRepository<User>, SqlRepository<User>>("sql");
services.AddKeyedScoped<IRepository<User>, CosmosRepository<User>>("cosmos");
// Each resolves with its own decorator chain
var sqlRepo = serviceProvider.GetRequiredKeyedService<IRepository<User>>("sql");
var cosmosRepo = serviceProvider.GetRequiredKeyedService<IRepository<User>>("cosmos");
Different Key Types¶
Keys can be any object type:
// String keys
services.AddKeyedScoped<IRepository<User>, UserRepository>("primary");
// Integer keys
services.AddKeyedScoped<IRepository<Order>, OrderRepository>(1);
// Enum keys
public enum DatabaseType { Primary, Secondary, Archive }
services.AddKeyedScoped<IRepository<Data>, DataRepository>(DatabaseType.Primary);
Keyed Services with Factory Delegates¶
Combine keyed services with factory delegates:
[DecoratedBy<CachingRepository<>>]
public class ConfigurableRepository<T> : IRepository<T>
{
private readonly string _connectionString;
public ConfigurableRepository(string connectionString)
{
_connectionString = connectionString;
}
// Implementation
}
// Keyed factory delegate
services.AddKeyedScoped<IRepository<Customer>, ConfigurableRepository<Customer>>(
"primary",
(sp, key) => new ConfigurableRepository<Customer>("Server=primary;Database=Main")
);
// Decorators still apply
var repo = serviceProvider.GetRequiredKeyedService<IRepository<Customer>>("primary");
// Returns: CachingRepository<Customer> wrapping ConfigurableRepository<Customer>
Keyed Service Lifetimes¶
All lifetimes are supported:
// Scoped
services.AddKeyedScoped<IRepository<User>, UserRepository>("scoped-key");
// Transient
services.AddKeyedTransient<IRepository<Event>, EventRepository>("transient-key");
// Singleton
services.AddKeyedSingleton<IRepository<Config>, ConfigRepository>("singleton-key");
How Keyed Services Work¶
When using keyed services with decorators:
- User's key is preserved - You resolve services using your original key
- Nested key prevents conflicts - DecoWeaver uses an internal nested key format
- Independent decoration - Each key gets its own decorator chain
// What you write:
services.AddKeyedScoped<IRepository<User>, SqlRepository<User>>("sql");
// What happens internally:
// 1. Nested key created: "sql|IRepository`1|SqlRepository`1"
// 2. Undecorated impl registered with nested key
// 3. Decorated factory registered with your key "sql"
// 4. When you resolve with "sql", decorators are applied
Keyed Service Consumer Injection¶
Use the [FromKeyedServices] attribute to inject keyed services:
public class UserService
{
private readonly IRepository<User> _sqlRepo;
private readonly IRepository<User> _cosmosRepo;
public UserService(
[FromKeyedServices("sql")] IRepository<User> sqlRepo,
[FromKeyedServices("cosmos")] IRepository<User> cosmosRepo)
{
_sqlRepo = sqlRepo; // Decorated SqlRepository
_cosmosRepo = cosmosRepo; // Decorated CosmosRepository
}
}
Keyed Service Limitations¶
Keyed services work with: - ✅ All registration patterns: parameterless and factory delegates - ✅ All lifetimes: Scoped, Transient, Singleton - ✅ All key types: string, int, enum, custom objects - ✅ Multiple keys per service type - ✅ Multiple decorators with ordering
Current limitations: - ❌ Instance registrations with keys - ❌ Open generic registration with typeof() syntax
Decorator Dependencies¶
Decorators can have their own dependencies, resolved from the DI container:
public class CachingRepository : IUserRepository
{
private readonly IUserRepository _inner;
private readonly IMemoryCache _cache; // Resolved from DI
private readonly ILogger _logger; // Resolved from DI
private readonly IOptions<CacheOptions> _options; // Resolved from DI
public CachingRepository(
IUserRepository inner,
IMemoryCache cache,
ILogger<CachingRepository> logger,
IOptions<CacheOptions> options)
{
_inner = inner;
_cache = cache;
_logger = logger;
_options = options;
}
// Implementation
}
Just make sure all dependencies are registered:
services.AddMemoryCache();
services.AddLogging();
services.Configure<CacheOptions>(config.GetSection("Cache"));
[DecoratedBy<CachingRepository>]
public class UserRepository : IUserRepository { }
services.AddScoped<IUserRepository, UserRepository>();
Multiple Interfaces¶
If your implementation implements multiple interfaces, decorators apply per interface:
public interface IReadRepository
{
Task<User> GetByIdAsync(int id);
}
public interface IWriteRepository
{
Task SaveAsync(User user);
}
// Decorates both interfaces
[DecoratedBy<LoggingDecorator>]
public class UserRepository : IReadRepository, IWriteRepository
{
public Task<User> GetByIdAsync(int id) { }
public Task SaveAsync(User user) { }
}
// Register each interface separately
services.AddScoped<IReadRepository, UserRepository>();
services.AddScoped<IWriteRepository, UserRepository>();
Both registrations will be decorated.
Inheritance¶
The [DecoratedBy] attribute is not inherited:
[DecoratedBy<LoggingDecorator>]
public class BaseRepository : IRepository { }
// This is NOT decorated - attribute doesn't inherit
public class UserRepository : BaseRepository { }
// You must apply it explicitly
[DecoratedBy<LoggingDecorator>]
public class UserRepository : BaseRepository { }
Closed Generic Types¶
For closed generic types, use the decorator directly:
// Closed generic implementation
[DecoratedBy<CachingUserRepository>]
public class UserRepository : IRepository<User>
{
// Implementation
}
// Closed generic decorator
public class CachingUserRepository : IRepository<User>
{
private readonly IRepository<User> _inner;
public CachingUserRepository(IRepository<User> inner, IMemoryCache cache)
{
_inner = inner;
}
}
// Registration
services.AddScoped<IRepository<User>, UserRepository>();
Order Property¶
Control the order when using multiple decorators:
[DecoratedBy<LoggingDecorator>(Order = 1)]
[DecoratedBy<CachingDecorator>(Order = 2)]
[DecoratedBy<MetricsDecorator>(Order = 3)]
public class UserRepository : IUserRepository { }
Lower order numbers are closer to the implementation. See Order and Nesting for details.
Common Patterns¶
Logging Decorator¶
public class LoggingDecorator<T> : T where T : class
{
private readonly T _inner;
private readonly ILogger _logger;
public LoggingDecorator(T inner, ILogger<LoggingDecorator<T>> logger)
{
_inner = inner;
_logger = logger;
}
// Implement all interface methods with logging
}
[DecoratedBy<LoggingDecorator<IUserRepository>>]
public class UserRepository : IUserRepository { }
Caching Decorator¶
public class CachingDecorator : IUserRepository
{
private readonly IUserRepository _inner;
private readonly IMemoryCache _cache;
public CachingDecorator(IUserRepository inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public async Task<User> GetByIdAsync(int id)
{
var key = $"user:{id}";
if (_cache.TryGetValue(key, out User cached))
return cached;
var user = await _inner.GetByIdAsync(id);
_cache.Set(key, user, TimeSpan.FromMinutes(5));
return user;
}
}
[DecoratedBy<CachingDecorator>]
public class UserRepository : IUserRepository { }
Metrics Decorator¶
public class MetricsDecorator : IUserRepository
{
private readonly IUserRepository _inner;
private readonly IMeterFactory _meterFactory;
public MetricsDecorator(IUserRepository inner, IMeterFactory meterFactory)
{
_inner = inner;
_meterFactory = meterFactory;
}
public async Task<User> GetByIdAsync(int id)
{
using var activity = _meterFactory.StartActivity("user.get");
try
{
var result = await _inner.GetByIdAsync(id);
activity.SetStatus(ActivityStatusCode.Ok);
return result;
}
catch
{
activity.SetStatus(ActivityStatusCode.Error);
throw;
}
}
}
[DecoratedBy<MetricsDecorator>]
public class UserRepository : IUserRepository { }
Troubleshooting¶
Decorator Not Applied¶
If your decorator isn't being applied:
- Check C# language version is set to 11 or higher in .csproj
- Verify the decorator implements the same interface
- Ensure all decorator dependencies are registered in DI
- Rebuild the solution to regenerate interceptor code
- Check for build errors in the Error List
Missing Dependencies¶
If you get runtime errors about missing dependencies:
// ❌ Error: IMemoryCache not registered
[DecoratedBy<CachingDecorator>]
public class UserRepository : IUserRepository { }
services.AddScoped<IUserRepository, UserRepository>();
// Missing: services.AddMemoryCache();
// ✅ Fixed: Register all dependencies
services.AddMemoryCache();
services.AddScoped<IUserRepository, UserRepository>();
Wrong Constructor Parameter¶
The decorated interface must be the first parameter (or properly identified):
// ❌ Incorrect: Interface not first
public class LoggingDecorator : IUserRepository
{
public LoggingDecorator(ILogger logger, IUserRepository inner) { }
}
// ✅ Correct: Interface first
public class LoggingDecorator : IUserRepository
{
public LoggingDecorator(IUserRepository inner, ILogger logger) { }
}
Best Practices¶
- Keep decorators focused on a single concern (logging, caching, etc.)
- Make decorators reusable with generic type parameters when possible
- Use meaningful order numbers (10, 20, 30 instead of 1, 2, 3) to allow insertion
- Document decorator behavior with XML comments
- Test decorators in isolation with mocked inner implementations
Comparison with Assembly-Level¶
| Aspect | Class-Level | Assembly-Level |
|---|---|---|
| Scope | Single implementation | All implementations |
| Attribute Location | On class | In global file |
| Use Case | Implementation-specific | Cross-cutting concerns |
| Visibility | More explicit | Less obvious |
| Flexibility | Full control | Can opt-out with [DoNotDecorate] |
When to Use Each¶
Use Class-Level When: - Decorator is specific to one implementation - You want explicit, visible decoration - Different implementations need different decorators - Testing or development-only decorators
Use Assembly-Level When: - Same decorator applies to many implementations - Enforcing cross-cutting concerns (logging, metrics, caching) - Centralizing decorator configuration - Ensuring consistency across implementations
Combine Both When: - Assembly-level for common concerns - Class-level for implementation-specific needs
// GlobalUsings.cs - Common logging for all
[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 10)]
// UserRepository.cs - Add validation specific to users
[DecoratedBy<ValidationRepository<User>>(Order = 5)]
public class UserRepository : IRepository<User> { }
Next Steps¶
- Learn about Assembly-Level Decorators for cross-cutting concerns
- Learn about Multiple Decorators
- Explore Open Generic Support
- See Examples of real-world usage