Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ services.AddScoped<IRepository<Product>, Repository<Product>>();
services.AddScoped<IRepository<User>, Repository<User>>(sp => new Repository<User>());
services.AddScoped<IRepository<User>>(sp => new Repository<User>());

// ✅ Keyed service registration - INTERCEPTED by DecoWeaver (v1.0.3+)
services.AddKeyedScoped<IRepository<User>, Repository<User>>("sql");
services.AddKeyedScoped<IRepository<User>, Repository<User>>("sql", (sp, key) => new Repository<User>());

// ❌ Open generic registration - NOT intercepted
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
```
Expand Down Expand Up @@ -217,6 +221,59 @@ services.AddScoped<IRepository<User>, Repository<User>>(sp =>

Decorators are applied around the factory result, and the factory logic is preserved.

### Keyed Service Support (v1.0.3+)

DecoWeaver supports keyed service registrations for all three lifetimes. Keyed services allow multiple implementations of the same service type to be registered with different keys.

**Keyed parameterless registration**:
```csharp
services.AddKeyedScoped<IRepository<User>, SqlRepository<User>>("sql");
services.AddKeyedScoped<IRepository<User>, CosmosRepository<User>>("cosmos");
services.AddKeyedTransient<ICache<T>, RedisCache<T>>("redis");
services.AddKeyedSingleton<ILogger<T>, FileLogger<T>>("file");
```

**Keyed with factory delegate**:
```csharp
// Two-parameter keyed factory: Func<IServiceProvider, object?, TService>
services.AddKeyedScoped<IRepository<User>, SqlRepository<User>>(
"sql",
(sp, key) => new SqlRepository<User>("Server=sql01;Database=Users")
);

// Single-parameter keyed factory: Func<IServiceProvider, object?, TService>
services.AddKeyedScoped<IRepository<User>>(
"sql",
(sp, key) => new SqlRepository<User>("Server=sql01;Database=Users")
);
```

**Nested key strategy**:
- User's original key is preserved for resolution via `GetRequiredKeyedService(userKey)`
- Internally, DecoWeaver creates a nested key: `"{userKey}|{ServiceAQN}|{ImplAQN}"`
- Undecorated implementation registered with nested key to prevent circular resolution
- Decorated factory registered with user's original key
- Each key gets independent decorator chain - no sharing between keys

**Example resolution**:
```csharp
// User code (exactly as before)
var sqlRepo = serviceProvider.GetRequiredKeyedService<IRepository<User>>("sql");

// What DecoWeaver generates internally:
// 1. Register undecorated: AddKeyedScoped<...>("sql|IRepository`1|SqlRepository`1")
// 2. Register decorated: AddKeyedScoped<...>("sql", (sp, key) => {
// var current = sp.GetRequiredKeyedService<...>("sql|IRepository`1|SqlRepository`1");
// current = DecoratorFactory.Create(sp, typeof(...), typeof(LoggingDecorator<>), current);
// return current;
// })
```

**Key type support**:
- All key types supported: `string`, `int`, `enum`, custom objects
- Multiple keys for same service type work independently
- Each keyed registration is intercepted separately

### Attribute Compilation

- Attributes marked with `[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")]`
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="releasenotes.props" />

<PropertyGroup>
<VersionPrefix>1.0.2-beta</VersionPrefix>
<VersionPrefix>1.0.3-beta</VersionPrefix>
<!-- SPDX license identifier for MIT -->
<PackageLicenseExpression>MIT</PackageLicenseExpression>

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ For more examples including open generics, multiple decorators, and ordering, se

- **Assembly-Level Decorators**: Apply decorators to all implementations from one place with `[assembly: DecorateService(...)]`
- **Class-Level Decorators**: Apply decorators to specific implementations with `[DecoratedBy<T>]`
- **Keyed Service Support**: Works with keyed service registrations like `AddKeyedScoped<T, Impl>(serviceKey)`
- **Factory Delegate Support**: Works with factory registrations like `AddScoped<T, Impl>(sp => new Impl(...))`
- **Opt-Out Support**: Exclude specific decorators with `[DoNotDecorate]`
- **Multiple Decorators**: Stack multiple decorators with explicit ordering
Expand Down
31 changes: 30 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- No changes yet

## [1.0.3-beta] - 2025-11-13

### Added
- **Keyed service support** - Decorators now work with keyed service registrations
- `AddKeyedScoped<TService, TImplementation>(serviceKey)` - Keyed parameterless registration
- `AddKeyedScoped<TService, TImplementation>(serviceKey, factory)` - Keyed with factory delegate (two-parameter)
- `AddKeyedScoped<TService>(serviceKey, factory)` - Keyed with factory delegate (single-parameter)
- All lifetimes supported: `AddKeyedScoped`, `AddKeyedTransient`, `AddKeyedSingleton`
- Multiple keys per service type work independently
- All key types supported: string, int, enum, custom objects
- Nested key strategy prevents circular resolution while preserving user's original key

### Changed
- Extended `RegistrationKind` enum with three keyed service variants
- Added `ServiceKeyParameterName` field to `ClosedGenericRegistration` model
- Updated `ClosedGenericRegistrationProvider` to detect keyed service signatures (2 or 3 parameters)
- Updated `InterceptorEmitter` to generate keyed service interceptors with nested key strategy
- Added `ForKeyed` helper method to `DecoratorKeys` class for nested key generation

### Technical Details
- Added `RegistrationKind` values: `KeyedParameterless`, `KeyedFactoryTwoTypeParams`, `KeyedFactorySingleTypeParam`
- Keyed service detection validates `object?` type for service key parameter
- Factory delegates with keyed services detect `Func<IServiceProvider, object?, T>` signatures
- Nested key format: `"{userKey}|{ServiceAQN}|{ImplAQN}"` prevents conflicts between keys
- User's original key preserved for resolution via `GetRequiredKeyedService`
- 7 new test cases (039-045) covering keyed service scenarios
- Updated sample project with keyed service examples (string and integer keys, multiple keys)
- All existing functionality remains unchanged - this is purely additive

## [1.0.2-beta] - 2025-11-12

### Added
Expand All @@ -35,7 +64,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Updated sample project with factory delegate examples demonstrating complex dependencies
- All existing functionality remains unchanged - this is purely additive

## [1.0.1-beta] - 2025-01-XX
## [1.0.1-beta] - 2025-11-10

### Added
- Assembly-level `[DecorateService(typeof(TService), typeof(TDecorator))]` attribute for applying decorators to all implementations of a service interface
Expand Down
158 changes: 157 additions & 1 deletion docs/usage/class-level-decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,171 @@ var logged = new LoggingRepository(cached);

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:
- ❌ Keyed service registrations with factory delegates
- ❌ Instance registrations
- ❌ Open generic registration with `typeof()` syntax

## Keyed Service Registration

!!! info "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

```csharp
[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:

```csharp
[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:

```csharp
// 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:

```csharp
[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:

```csharp
// 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:

1. **User's key is preserved** - You resolve services using your original key
2. **Nested key prevents conflicts** - DecoWeaver uses an internal nested key format
3. **Independent decoration** - Each key gets its own decorator chain

```csharp
// 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:

```csharp
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:
Expand Down
43 changes: 43 additions & 0 deletions releasenotes.props
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,49 @@ This release adds support for factory delegate registrations, addressing issue #
* Full documentation: https://layeredcraft.github.io/decoweaver/
* GitHub: https://github.com/layeredcraft/decoweaver

]]>
</PackageReleaseNotes>

<PackageReleaseNotes Condition="$(VersionPrefix.StartsWith('1.0.3-beta'))">
<![CDATA[
## DecoWeaver 1.0.3-beta - Keyed Service Support

This release adds support for keyed service registrations, completing issue #3 (Phase 2).

### New Features

* **Keyed service support** - Decorators now work with keyed service registrations:
- `AddKeyedScoped<TService, TImpl>(serviceKey)` - Keyed parameterless registration
- `AddKeyedScoped<TService, TImpl>(serviceKey, factory)` - Keyed with factory delegate (two-parameter)
- `AddKeyedScoped<TService>(serviceKey, factory)` - Keyed with factory delegate (single-parameter)
- All lifetimes supported: `AddKeyedScoped`, `AddKeyedTransient`, `AddKeyedSingleton`
* **Multiple keys per service** - Register the same service type with different keys independently
* **All key types supported** - Works with `string`, `int`, `enum`, and custom object keys
* **Nested key strategy** - Prevents circular resolution while preserving user's original key

### What's Changed

* Extended `RegistrationKind` enum with three keyed service variants
* Added `ServiceKeyParameterName` field to `ClosedGenericRegistration` model
* Updated `ClosedGenericRegistrationProvider` to detect keyed service signatures (2 or 3 parameters)
* Updated `InterceptorEmitter` to generate keyed service interceptors with nested key strategy
* Added `ForKeyed` helper method to `DecoratorKeys` class for nested key generation

### Technical Details

* Nested key format: `"{userKey}|{ServiceAQN}|{ImplAQN}"` prevents conflicts between keys
* User's original key preserved for resolution via `GetRequiredKeyedService`
* Each key gets independent decorator chain - no sharing between keys
* 7 new test cases (039-045) covering keyed service scenarios
* Updated sample project with keyed service examples (string and integer keys, multiple keys)
* All existing functionality remains unchanged - this is purely additive

### Documentation

* Full documentation: https://layeredcraft.github.io/decoweaver/
* Changelog: https://layeredcraft.github.io/decoweaver/changelog/
* GitHub: https://github.com/layeredcraft/decoweaver

]]>
</PackageReleaseNotes>

Expand Down
24 changes: 24 additions & 0 deletions samples/DecoWeaver.Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
var logger = loggerFactory.CreateLogger<RepositoryWithLogger<Product>>();
return new RepositoryWithLogger<Product>(logger);
})
// Example 5: Keyed service with string key
.AddKeyedScoped<IAssemblyInterface<string>, ConcreteClass<string>>("primary")
// Example 6: Multiple keyed services with different keys
.AddKeyedScoped<IAssemblyInterface<int>, ConcreteClass<int>>("cache")
.AddKeyedScoped<IAssemblyInterface<int>, ConcreteClass<int>>("database")
.BuildServiceProvider();

// Test Example 1: Open generic repository (parameterless)
Expand Down Expand Up @@ -49,6 +54,25 @@
var productRepo = serviceProvider.GetRequiredService<IRepository<Product>>();
Console.WriteLine($"Resolved: {productRepo.GetType().Name}");
productRepo.Save(new Product { Id = 1, Name = "Widget" });
Console.WriteLine();

// Test Example 5: Keyed service with string key
Console.WriteLine("=== Example 5: Keyed Service (String Key) ===");
var primaryService = serviceProvider.GetRequiredKeyedService<IAssemblyInterface<string>>("primary");
Console.WriteLine($"Resolved: {primaryService.GetType().Name}");
primaryService.DoSomething("Hello from primary");
Console.WriteLine();

// Test Example 6: Multiple keyed services with different keys
Console.WriteLine("=== Example 6: Keyed Services (Multiple Keys) ===");
var cacheService = serviceProvider.GetRequiredKeyedService<IAssemblyInterface<int>>("cache");
Console.WriteLine($"Resolved 'cache': {cacheService.GetType().Name}");
cacheService.DoSomething(42);
Console.WriteLine();

var databaseService = serviceProvider.GetRequiredKeyedService<IAssemblyInterface<int>>("database");
Console.WriteLine($"Resolved 'database': {databaseService.GetType().Name}");
databaseService.DoSomething(100);

public class Customer
{
Expand Down
Loading
Loading