diff --git a/CLAUDE.md b/CLAUDE.md index 2611778..0b0cac1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -185,6 +185,10 @@ services.AddScoped, Repository>(); services.AddScoped, Repository>(sp => new Repository()); services.AddScoped>(sp => new Repository()); +// ✅ Keyed service registration - INTERCEPTED by DecoWeaver (v1.0.3+) +services.AddKeyedScoped, Repository>("sql"); +services.AddKeyedScoped, Repository>("sql", (sp, key) => new Repository()); + // ❌ Open generic registration - NOT intercepted services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); ``` @@ -217,6 +221,59 @@ services.AddScoped, Repository>(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, SqlRepository>("sql"); +services.AddKeyedScoped, CosmosRepository>("cosmos"); +services.AddKeyedTransient, RedisCache>("redis"); +services.AddKeyedSingleton, FileLogger>("file"); +``` + +**Keyed with factory delegate**: +```csharp +// Two-parameter keyed factory: Func +services.AddKeyedScoped, SqlRepository>( + "sql", + (sp, key) => new SqlRepository("Server=sql01;Database=Users") +); + +// Single-parameter keyed factory: Func +services.AddKeyedScoped>( + "sql", + (sp, key) => new SqlRepository("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>("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")]` diff --git a/Directory.Build.props b/Directory.Build.props index 4001cbd..389c18a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ - 1.0.2-beta + 1.0.3-beta MIT diff --git a/README.md b/README.md index 69a38bb..f891c93 100644 --- a/README.md +++ b/README.md @@ -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]` +- **Keyed Service Support**: Works with keyed service registrations like `AddKeyedScoped(serviceKey)` - **Factory Delegate Support**: Works with factory registrations like `AddScoped(sp => new Impl(...))` - **Opt-Out Support**: Exclude specific decorators with `[DoNotDecorate]` - **Multiple Decorators**: Stack multiple decorators with explicit ordering diff --git a/docs/changelog.md b/docs/changelog.md index c15b68b..09bdc3f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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(serviceKey)` - Keyed parameterless registration + - `AddKeyedScoped(serviceKey, factory)` - Keyed with factory delegate (two-parameter) + - `AddKeyedScoped(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` 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 @@ -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 diff --git a/docs/usage/class-level-decorators.md b/docs/usage/class-level-decorators.md index 3b0fa32..70eb7ed 100644 --- a/docs/usage/class-level-decorators.md +++ b/docs/usage/class-level-decorators.md @@ -210,15 +210,171 @@ var logged = new LoggingRepository(cached); Factory delegates work with: - ✅ Generic registration methods: `AddScoped(factory)`, `AddScoped(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>] +public class SqlRepository : IRepository +{ + // Your SQL implementation +} + +// Register with a key +services.AddKeyedScoped, SqlRepository>("sql"); + +// Resolve using the key +var repo = serviceProvider.GetRequiredKeyedService>("sql"); +// Returns: LoggingRepository wrapping SqlRepository +``` + +### Multiple Keys for Same Service + +Register multiple implementations with different keys - each gets decorated independently: + +```csharp +[DecoratedBy>] +public class SqlRepository : IRepository { } + +[DecoratedBy>] +public class CosmosRepository : IRepository { } + +// Register both with different keys +services.AddKeyedScoped, SqlRepository>("sql"); +services.AddKeyedScoped, CosmosRepository>("cosmos"); + +// Each resolves with its own decorator chain +var sqlRepo = serviceProvider.GetRequiredKeyedService>("sql"); +var cosmosRepo = serviceProvider.GetRequiredKeyedService>("cosmos"); +``` + +### Different Key Types + +Keys can be any object type: + +```csharp +// String keys +services.AddKeyedScoped, UserRepository>("primary"); + +// Integer keys +services.AddKeyedScoped, OrderRepository>(1); + +// Enum keys +public enum DatabaseType { Primary, Secondary, Archive } +services.AddKeyedScoped, DataRepository>(DatabaseType.Primary); +``` + +### Keyed Services with Factory Delegates + +Combine keyed services with factory delegates: + +```csharp +[DecoratedBy>] +public class ConfigurableRepository : IRepository +{ + private readonly string _connectionString; + + public ConfigurableRepository(string connectionString) + { + _connectionString = connectionString; + } + + // Implementation +} + +// Keyed factory delegate +services.AddKeyedScoped, ConfigurableRepository>( + "primary", + (sp, key) => new ConfigurableRepository("Server=primary;Database=Main") +); + +// Decorators still apply +var repo = serviceProvider.GetRequiredKeyedService>("primary"); +// Returns: CachingRepository wrapping ConfigurableRepository +``` + +### Keyed Service Lifetimes + +All lifetimes are supported: + +```csharp +// Scoped +services.AddKeyedScoped, UserRepository>("scoped-key"); + +// Transient +services.AddKeyedTransient, EventRepository>("transient-key"); + +// Singleton +services.AddKeyedSingleton, 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, SqlRepository>("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 _sqlRepo; + private readonly IRepository _cosmosRepo; + + public UserService( + [FromKeyedServices("sql")] IRepository sqlRepo, + [FromKeyedServices("cosmos")] IRepository 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: diff --git a/releasenotes.props b/releasenotes.props index b48baf3..63dc89d 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -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 +]]> + + + + (serviceKey)` - Keyed parameterless registration + - `AddKeyedScoped(serviceKey, factory)` - Keyed with factory delegate (two-parameter) + - `AddKeyedScoped(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 + ]]> diff --git a/samples/DecoWeaver.Sample/Program.cs b/samples/DecoWeaver.Sample/Program.cs index 8759864..cfb4633 100644 --- a/samples/DecoWeaver.Sample/Program.cs +++ b/samples/DecoWeaver.Sample/Program.cs @@ -21,6 +21,11 @@ var logger = loggerFactory.CreateLogger>(); return new RepositoryWithLogger(logger); }) + // Example 5: Keyed service with string key + .AddKeyedScoped, ConcreteClass>("primary") + // Example 6: Multiple keyed services with different keys + .AddKeyedScoped, ConcreteClass>("cache") + .AddKeyedScoped, ConcreteClass>("database") .BuildServiceProvider(); // Test Example 1: Open generic repository (parameterless) @@ -49,6 +54,25 @@ var productRepo = serviceProvider.GetRequiredService>(); 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>("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>("cache"); +Console.WriteLine($"Resolved 'cache': {cacheService.GetType().Name}"); +cacheService.DoSomething(42); +Console.WriteLine(); + +var databaseService = serviceProvider.GetRequiredKeyedService>("database"); +Console.WriteLine($"Resolved 'database': {databaseService.GetType().Name}"); +databaseService.DoSomething(100); public class Customer { diff --git a/samples/DecoWeaver.Sample/Repository.cs b/samples/DecoWeaver.Sample/Repository.cs index b03d761..2b21cc6 100644 --- a/samples/DecoWeaver.Sample/Repository.cs +++ b/samples/DecoWeaver.Sample/Repository.cs @@ -113,6 +113,8 @@ public interface IAssemblyInterface void DoSomething(T item); } +[DecoratedBy(typeof(LoggingDecorator<>), Order = 2)] +[DecoratedBy(typeof(CachingDecorator<>), Order = 1)] public sealed class ConcreteClass : IAssemblyInterface { public void DoSomething(T item) diff --git a/src/LayeredCraft.DecoWeaver.Attributes/LayeredCraft.DecoWeaver.Attributes.csproj b/src/LayeredCraft.DecoWeaver.Attributes/LayeredCraft.DecoWeaver.Attributes.csproj index c439861..becc141 100644 --- a/src/LayeredCraft.DecoWeaver.Attributes/LayeredCraft.DecoWeaver.Attributes.csproj +++ b/src/LayeredCraft.DecoWeaver.Attributes/LayeredCraft.DecoWeaver.Attributes.csproj @@ -11,7 +11,7 @@ true - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs b/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs index d3ea617..0355fe1 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs @@ -102,6 +102,18 @@ private static void EmitSingleInterceptor( EmitFactorySingleTypeParamInterceptor(sb, methodName, reg, methodIndex, serviceFqn, implFqn, decorators); break; + case RegistrationKind.KeyedParameterless: + EmitKeyedParameterlessInterceptor(sb, methodName, reg, methodIndex, serviceFqn, implFqn, decorators); + break; + + case RegistrationKind.KeyedFactoryTwoTypeParams: + EmitKeyedFactoryTwoTypeParamsInterceptor(sb, methodName, reg, methodIndex, serviceFqn, implFqn, decorators); + break; + + case RegistrationKind.KeyedFactorySingleTypeParam: + EmitKeyedFactorySingleTypeParamInterceptor(sb, methodName, reg, methodIndex, serviceFqn, implFqn, decorators); + break; + default: throw new InvalidOperationException($"Unsupported registration kind: {reg.Kind}"); } @@ -238,6 +250,148 @@ private static void EmitFactorySingleTypeParamInterceptor( sb.AppendLine(); } + private static void EmitKeyedParameterlessInterceptor( + StringBuilder sb, + string methodName, + ClosedGenericRegistration reg, + int methodIndex, + string serviceFqn, + string implFqn, + string[] decorators) + { + var serviceKeyParamName = reg.ServiceKeyParameterName ?? "serviceKey"; + + // Emit the method signature matching AddKeyedScoped(services, object? serviceKey) + sb.AppendLine($" /// Intercepted: ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}, {implFqn}>(IServiceCollection, object?)"); + sb.AppendLine($" internal static IServiceCollection {methodName}_{methodIndex}(this IServiceCollection services, object? {serviceKeyParamName})"); + sb.AppendLine(" where TService : class"); + sb.AppendLine(" where TImplementation : class, TService"); + sb.AppendLine(" {"); + + if (decorators.Length > 0) + { + sb.AppendLine(" // Create nested key to avoid circular resolution"); + sb.AppendLine($" var nestedKey = DecoratorKeys.ForKeyed({serviceKeyParamName}, typeof({serviceFqn}), typeof({implFqn}));"); + sb.AppendLine(); + sb.AppendLine(" // Register the undecorated implementation with nested key"); + sb.AppendLine($" services.{methodName}<{serviceFqn}, {implFqn}>(nestedKey);"); + sb.AppendLine(); + sb.AppendLine(" // Register factory with user's key that applies decorators"); + sb.AppendLine($" services.{methodName}<{serviceFqn}>({serviceKeyParamName}, (sp, key) =>"); + sb.AppendLine(" {"); + sb.AppendLine($" var current = ({serviceFqn})sp.GetRequiredKeyedService<{serviceFqn}>(nestedKey)!;"); + sb.AppendLine(" // Compose decorators (innermost to outermost)"); + foreach (var deco in decorators) + sb.AppendLine($" current = ({serviceFqn})DecoratorFactory.Create(sp, typeof({serviceFqn}), typeof({deco}), current);"); + sb.AppendLine(" return current;"); + sb.AppendLine(" });"); + sb.AppendLine(" return services;"); + } + else + { + sb.AppendLine(" // No decorators, just register normally"); + sb.AppendLine($" return Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}, {implFqn}>(services, {serviceKeyParamName});"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } + + private static void EmitKeyedFactoryTwoTypeParamsInterceptor( + StringBuilder sb, + string methodName, + ClosedGenericRegistration reg, + int methodIndex, + string serviceFqn, + string implFqn, + string[] decorators) + { + var serviceKeyParamName = reg.ServiceKeyParameterName ?? "serviceKey"; + var factoryParamName = reg.FactoryParameterName ?? "implementationFactory"; + + // Emit the method signature matching AddKeyedScoped(services, object? serviceKey, Func) + sb.AppendLine($" /// Intercepted: ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}, {implFqn}>(IServiceCollection, object?, Func<IServiceProvider, object?, {implFqn}>)"); + sb.AppendLine($" internal static IServiceCollection {methodName}_{methodIndex}(this IServiceCollection services, object? {serviceKeyParamName}, Func {factoryParamName})"); + sb.AppendLine(" where TService : class"); + sb.AppendLine(" where TImplementation : class, TService"); + sb.AppendLine(" {"); + + if (decorators.Length > 0) + { + sb.AppendLine(" // Create nested key to avoid circular resolution"); + sb.AppendLine($" var nestedKey = DecoratorKeys.ForKeyed({serviceKeyParamName}, typeof({serviceFqn}), typeof({implFqn}));"); + sb.AppendLine(); + sb.AppendLine(" // Register the undecorated implementation with nested key and factory"); + sb.AppendLine($" services.{methodName}<{serviceFqn}>(nestedKey, (sp, key) => ({serviceFqn}){factoryParamName}(sp, {serviceKeyParamName}));"); + sb.AppendLine(); + sb.AppendLine(" // Register factory with user's key that applies decorators"); + sb.AppendLine($" services.{methodName}<{serviceFqn}>({serviceKeyParamName}, (sp, key) =>"); + sb.AppendLine(" {"); + sb.AppendLine($" var current = ({serviceFqn})sp.GetRequiredKeyedService<{serviceFqn}>(nestedKey)!;"); + sb.AppendLine(" // Compose decorators (innermost to outermost)"); + foreach (var deco in decorators) + sb.AppendLine($" current = ({serviceFqn})DecoratorFactory.Create(sp, typeof({serviceFqn}), typeof({deco}), current);"); + sb.AppendLine(" return current;"); + sb.AppendLine(" });"); + sb.AppendLine(" return services;"); + } + else + { + sb.AppendLine(" // No decorators, pass through to original method with factory"); + sb.AppendLine($" return Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}, {implFqn}>(services, {serviceKeyParamName}, {factoryParamName});"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } + + private static void EmitKeyedFactorySingleTypeParamInterceptor( + StringBuilder sb, + string methodName, + ClosedGenericRegistration reg, + int methodIndex, + string serviceFqn, + string implFqn, + string[] decorators) + { + var serviceKeyParamName = reg.ServiceKeyParameterName ?? "serviceKey"; + var factoryParamName = reg.FactoryParameterName ?? "implementationFactory"; + + // Emit the method signature matching AddKeyedScoped(services, object? serviceKey, Func) + sb.AppendLine($" /// Intercepted: ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}>(IServiceCollection, object?, Func<IServiceProvider, object?, {serviceFqn}>)"); + sb.AppendLine($" internal static IServiceCollection {methodName}_{methodIndex}(this IServiceCollection services, object? {serviceKeyParamName}, Func {factoryParamName})"); + sb.AppendLine(" where TService : class"); + sb.AppendLine(" {"); + + if (decorators.Length > 0) + { + sb.AppendLine(" // Create nested key to avoid circular resolution"); + sb.AppendLine($" var nestedKey = DecoratorKeys.ForKeyed({serviceKeyParamName}, typeof({serviceFqn}), typeof({implFqn}));"); + sb.AppendLine(); + sb.AppendLine(" // Register the undecorated implementation with nested key and factory"); + sb.AppendLine($" services.{methodName}<{serviceFqn}>(nestedKey, (sp, key) => {factoryParamName}(sp, {serviceKeyParamName}));"); + sb.AppendLine(); + sb.AppendLine(" // Register factory with user's key that applies decorators"); + sb.AppendLine($" services.{methodName}<{serviceFqn}>({serviceKeyParamName}, (sp, key) =>"); + sb.AppendLine(" {"); + sb.AppendLine($" var current = ({serviceFqn})sp.GetRequiredKeyedService<{serviceFqn}>(nestedKey)!;"); + sb.AppendLine(" // Compose decorators (innermost to outermost)"); + foreach (var deco in decorators) + sb.AppendLine($" current = ({serviceFqn})DecoratorFactory.Create(sp, typeof({serviceFqn}), typeof({deco}), current);"); + sb.AppendLine(" return current;"); + sb.AppendLine(" });"); + sb.AppendLine(" return services;"); + } + else + { + sb.AppendLine(" // No decorators, pass through to original method with factory"); + sb.AppendLine($" return Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}>(services, {serviceKeyParamName}, {factoryParamName});"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } + private static string AddKeyed(string lifetimeMethod) => lifetimeMethod switch { @@ -256,6 +410,13 @@ private static void EmitHelpers(StringBuilder sb) sb.AppendLine(" var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name;"); sb.AppendLine(" return string.Concat(s, \"|\", i);"); sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" public static object ForKeyed(object? userKey, Type serviceType, Type implementationType)"); + sb.AppendLine(" {"); + sb.AppendLine(" // Return a tuple that preserves the actual key object (not its string representation)"); + sb.AppendLine(" // This ensures distinct object keys create distinct nested keys"); + sb.AppendLine(" return (userKey, serviceType, implementationType);"); + sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); diff --git a/src/LayeredCraft.DecoWeaver.Generators/LayeredCraft.DecoWeaver.Generators.csproj b/src/LayeredCraft.DecoWeaver.Generators/LayeredCraft.DecoWeaver.Generators.csproj index 10a2824..2c0a948 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/LayeredCraft.DecoWeaver.Generators.csproj +++ b/src/LayeredCraft.DecoWeaver.Generators/LayeredCraft.DecoWeaver.Generators.csproj @@ -19,7 +19,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs index 65631ef..b122bea 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs @@ -12,7 +12,13 @@ internal enum RegistrationKind /// Factory with two type params: AddScoped<T1, T2>(Func<IServiceProvider, T2>) FactoryTwoTypeParams, /// Factory with single type param: AddScoped<T>(Func<IServiceProvider, T>) - FactorySingleTypeParam + FactorySingleTypeParam, + /// Keyed parameterless: AddKeyedScoped<T1, T2>(object? serviceKey) + KeyedParameterless, + /// Keyed factory with two type params: AddKeyedScoped<T1, T2>(object? serviceKey, Func<IServiceProvider, object?, T2>) + KeyedFactoryTwoTypeParams, + /// Keyed factory with single type param: AddKeyedScoped<T>(object? serviceKey, Func<IServiceProvider, object?, T>) + KeyedFactorySingleTypeParam } internal readonly record struct ClosedGenericRegistration( @@ -20,10 +26,17 @@ internal readonly record struct ClosedGenericRegistration( TypeDefId ImplDef, string ServiceFqn, // Fully qualified closed type (e.g., "global::DecoWeaver.Sample.IRepository") string ImplFqn, // Fully qualified closed type - string Lifetime, // "AddTransient" | "AddScoped" | "AddSingleton" + string Lifetime, // "AddTransient" | "AddScoped" | "AddSingleton" | "AddKeyedTransient" | "AddKeyedScoped" | "AddKeyedSingleton" string InterceptsData, // "file|start|length" RegistrationKind Kind = RegistrationKind.Parameterless, - string? FactoryParameterName = null // Parameter name from the original registration (e.g., "implementationFactory") + string? FactoryParameterName = null, // Parameter name from the original registration (e.g., "implementationFactory") + string? ServiceKeyParameterName = null // Parameter name for keyed services (e.g., "serviceKey") +); + +internal readonly record struct RegistrationValidationResult( + RegistrationKind Kind, + string? FactoryParameterName, + string? ServiceKeyParameterName ); /// @@ -56,114 +69,214 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) return null; var name = symbol.Name; - if (name is not ("AddTransient" or "AddScoped" or "AddSingleton")) return null; + var isKeyed = name is "AddKeyedTransient" or "AddKeyedScoped" or "AddKeyedSingleton"; + var isNonKeyed = name is "AddTransient" or "AddScoped" or "AddSingleton"; + + if (!isKeyed && !isNonKeyed) return null; // Must be generic method with 1 or 2 type arguments - // 2 type args: AddScoped() or AddScoped(factory) - // 1 type arg: AddScoped(factory) if (!symbol.IsGenericMethod) return null; if (symbol.TypeArguments.Length is not (1 or 2)) return null; - // symbolToCheck is the unreduced (static extension) form, so it has IServiceCollection as first parameter + // Validate registration pattern and extract metadata + var validationResult = isKeyed + ? ValidateKeyedRegistration(symbolToCheck, symbol) + : ValidateNonKeyedRegistration(symbolToCheck, symbol); + + if (validationResult is null) return null; + + // Extract service and implementation types + var (svc, impl) = ExtractServiceAndImplementationTypes(symbol); + if (svc is null || impl is null) return null; + + // Get interceptable location + var interceptsData = GetInterceptsData(ctx.SemanticModel, inv); + if (interceptsData is null) return null; + + // Generate fully qualified names for the closed types + var serviceFqn = svc.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var implFqn = impl.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + return new ClosedGenericRegistration( + ServiceDef: TypeId.Create(svc).Definition, + ImplDef: TypeId.Create(impl).Definition, + ServiceFqn: serviceFqn, + ImplFqn: implFqn, + Lifetime: name, + InterceptsData: interceptsData, + Kind: validationResult.Value.Kind, + FactoryParameterName: validationResult.Value.FactoryParameterName, + ServiceKeyParameterName: validationResult.Value.ServiceKeyParameterName + ); + } + + private static RegistrationValidationResult? ValidateNonKeyedRegistration( + IMethodSymbol symbolToCheck, + IMethodSymbol symbol) + { + // NON-KEYED SERVICES // Accept: // - 1 param: Parameterless (IServiceCollection services) // - 2 params: Factory delegate (IServiceCollection services, Func implementationFactory) - // Reject: Keyed services, instance registrations, non-generic overloads var paramCount = symbolToCheck.Parameters.Length; if (paramCount is not (1 or 2)) return null; - RegistrationKind kind; - string? factoryParamName = null; - if (paramCount == 1) { // Parameterless registration: AddScoped() - // Must have 2 type arguments if (symbol.TypeArguments.Length != 2) return null; - kind = RegistrationKind.Parameterless; + return new RegistrationValidationResult(RegistrationKind.Parameterless, null, null); } - else // paramCount == 2 + + // Factory delegate registration (paramCount == 2) + var factoryParam = symbolToCheck.Parameters[1]; + if (!ValidateFactoryDelegate(factoryParam, expectedArgCount: 2, out var funcReturnType)) + return null; + + var factoryParamName = factoryParam.Name; + + if (symbol.TypeArguments.Length == 2) { - // Factory delegate registration - // Second parameter must be Func - var factoryParam = symbolToCheck.Parameters[1]; - - // Check if parameter type is Func - if (factoryParam.Type is not INamedTypeSymbol funcType) return null; - if (funcType.OriginalDefinition.ToDisplayString() != "System.Func") return null; - if (funcType.TypeArguments.Length != 2) return null; - - var funcArgType = funcType.TypeArguments[0]; - var funcReturnType = funcType.TypeArguments[1]; - - // First arg must be IServiceProvider - if (funcArgType.ToDisplayString() != "System.IServiceProvider") return null; - - factoryParamName = factoryParam.Name; - - if (symbol.TypeArguments.Length == 2) - { - // AddScoped(Func) - kind = RegistrationKind.FactoryTwoTypeParams; - - // Verify factory return type matches TImplementation (second type arg) - var implTypeArg = symbol.TypeArguments[1]; - if (!SymbolEqualityComparer.Default.Equals(funcReturnType, implTypeArg)) - { - // Some overloads allow Func instead of TImplementation - // This is valid, but we use the TImplementation from type args for decorator matching - } - } - else // symbol.TypeArguments.Length == 1 - { - // AddScoped(Func) - kind = RegistrationKind.FactorySingleTypeParam; - - // Verify factory return type matches TService (only type arg) - var svcTypeArg = symbol.TypeArguments[0]; - if (!SymbolEqualityComparer.Default.Equals(funcReturnType, svcTypeArg)) - return null; // Invalid signature - } + // AddScoped(Func) + return new RegistrationValidationResult( + RegistrationKind.FactoryTwoTypeParams, + factoryParamName, + null); } - // Extract service and implementation types - INamedTypeSymbol? svc, impl; + // AddScoped(Func) + // Verify factory return type matches TService (only type arg) + var svcTypeArg = symbol.TypeArguments[0]; + if (!SymbolEqualityComparer.Default.Equals(funcReturnType, svcTypeArg)) + return null; + + return new RegistrationValidationResult( + RegistrationKind.FactorySingleTypeParam, + factoryParamName, + null); + } + + private static RegistrationValidationResult? ValidateKeyedRegistration( + IMethodSymbol symbolToCheck, + IMethodSymbol symbol) + { + // KEYED SERVICES + // Accept: + // - 2 params: Keyed parameterless (IServiceCollection services, object? serviceKey) + // - 3 params: Keyed factory delegate (IServiceCollection services, object? serviceKey, Func) + var paramCount = symbolToCheck.Parameters.Length; + if (paramCount is not (2 or 3)) return null; + + // Second parameter must be the service key (object?) + var keyParam = symbolToCheck.Parameters[1]; + if (keyParam.Type.SpecialType != SpecialType.System_Object) return null; + var serviceKeyParamName = keyParam.Name; + + if (paramCount == 2) + { + // Keyed parameterless: AddKeyedScoped(key) + if (symbol.TypeArguments.Length != 2) return null; + return new RegistrationValidationResult( + RegistrationKind.KeyedParameterless, + null, + serviceKeyParamName); + } + + // Keyed factory delegate registration (paramCount == 3) + var factoryParam = symbolToCheck.Parameters[2]; + if (!ValidateKeyedFactoryDelegate(factoryParam, out var funcReturnType)) + return null; + + var factoryParamName = factoryParam.Name; if (symbol.TypeArguments.Length == 2) { - svc = symbol.TypeArguments[0] as INamedTypeSymbol; - impl = symbol.TypeArguments[1] as INamedTypeSymbol; + // AddKeyedScoped(key, Func) + return new RegistrationValidationResult( + RegistrationKind.KeyedFactoryTwoTypeParams, + factoryParamName, + serviceKeyParamName); } - else // symbol.TypeArguments.Length == 1 + + // AddKeyedScoped(key, Func) + // Verify factory return type matches TService (only type arg) + var svcTypeArg = symbol.TypeArguments[0]; + if (!SymbolEqualityComparer.Default.Equals(funcReturnType, svcTypeArg)) + return null; + + return new RegistrationValidationResult( + RegistrationKind.KeyedFactorySingleTypeParam, + factoryParamName, + serviceKeyParamName); + } + + private static bool ValidateFactoryDelegate( + IParameterSymbol factoryParam, + int expectedArgCount, + out ITypeSymbol? funcReturnType) + { + funcReturnType = null; + + // Check if parameter type is Func + if (factoryParam.Type is not INamedTypeSymbol funcType) return false; + if (funcType.OriginalDefinition.ToDisplayString() != "System.Func") return false; + if (funcType.TypeArguments.Length != expectedArgCount) return false; + + var funcArgType = funcType.TypeArguments[0]; + + // First arg must be IServiceProvider + if (funcArgType.ToDisplayString() != "System.IServiceProvider") return false; + + funcReturnType = funcType.TypeArguments[1]; + return true; + } + + private static bool ValidateKeyedFactoryDelegate( + IParameterSymbol factoryParam, + out ITypeSymbol? funcReturnType) + { + funcReturnType = null; + + // Check if parameter type is Func + if (factoryParam.Type is not INamedTypeSymbol funcType) return false; + if (funcType.OriginalDefinition.ToDisplayString() != "System.Func") return false; + if (funcType.TypeArguments.Length != 3) return false; + + var funcArg1Type = funcType.TypeArguments[0]; + var funcArg2Type = funcType.TypeArguments[1]; + + // First arg must be IServiceProvider + if (funcArg1Type.ToDisplayString() != "System.IServiceProvider") return false; + + // Second arg must be object? (the key) + if (funcArg2Type.SpecialType != SpecialType.System_Object) return false; + + funcReturnType = funcType.TypeArguments[2]; + return true; + } + + private static (INamedTypeSymbol? Service, INamedTypeSymbol? Implementation) ExtractServiceAndImplementationTypes( + IMethodSymbol symbol) + { + if (symbol.TypeArguments.Length == 2) { - // For single type param factory: AddScoped(factory) - // Both service and implementation are the same type - svc = symbol.TypeArguments[0] as INamedTypeSymbol; - impl = svc; + var svc = symbol.TypeArguments[0] as INamedTypeSymbol; + var impl = symbol.TypeArguments[1] as INamedTypeSymbol; + return (svc, impl); } - if (svc is null || impl is null) return null; + // For single type param factory: AddScoped(factory) + // Both service and implementation are the same type + var type = symbol.TypeArguments[0] as INamedTypeSymbol; + return (type, type); + } + private static string? GetInterceptsData(SemanticModel semanticModel, InvocationExpressionSyntax inv) + { // Hash-based interceptable location (Roslyn experimental) #pragma warning disable RSEXPERIMENTAL002 - var il = ctx.SemanticModel.GetInterceptableLocation(inv); + var il = semanticModel.GetInterceptableLocation(inv); #pragma warning restore RSEXPERIMENTAL002 - if (il is null) return null; - - // Generate fully qualified names for the closed types - // FullyQualifiedFormat includes global:: for ALL types including nested generic type arguments - var serviceFqn = svc.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - var implFqn = impl.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - return new ClosedGenericRegistration( - ServiceDef: TypeId.Create(svc).Definition, - ImplDef: TypeId.Create(impl).Definition, - ServiceFqn: serviceFqn, - ImplFqn: implFqn, - Lifetime: name, - InterceptsData: il.Data, - Kind: kind, - FactoryParameterName: factoryParamName - ); + return il?.Data; } -} +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/039_KeyedService_SingleDecorator/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/039_KeyedService_SingleDecorator/Program.cs new file mode 100644 index 0000000..34d265c --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/039_KeyedService_SingleDecorator/Program.cs @@ -0,0 +1,11 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Keyed service registration with single decorator +services.AddKeyedScoped, SqlRepository>("sql"); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredKeyedService>("sql"); +repo.Save(new Customer { Id = 1, Name = "John" }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/039_KeyedService_SingleDecorator/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/039_KeyedService_SingleDecorator/Repository.cs new file mode 100644 index 0000000..4922a00 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/039_KeyedService_SingleDecorator/Repository.cs @@ -0,0 +1,39 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +[DecoratedBy(typeof(LoggingRepository<>))] +public class SqlRepository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[SQL] Saving {typeof(T).Name}..."); + } +} + +public class LoggingRepository : IRepository +{ + private readonly IRepository _inner; + + public LoggingRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Log] Save called for {typeof(T).Name}"); + _inner.Save(entity); + } +} + +public class Customer +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/040_KeyedService_MultipleKeys/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/040_KeyedService_MultipleKeys/Program.cs new file mode 100644 index 0000000..40b117e --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/040_KeyedService_MultipleKeys/Program.cs @@ -0,0 +1,17 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Multiple keyed services with same interface but different keys +services.AddKeyedScoped, SqlRepository>("sql"); +services.AddKeyedScoped, CosmosRepository>("cosmos"); + +var serviceProvider = services.BuildServiceProvider(); + +// Resolve each by key - both should be decorated independently +var sqlRepo = serviceProvider.GetRequiredKeyedService>("sql"); +sqlRepo.Save(new User { Id = 1, Name = "John" }); + +var cosmosRepo = serviceProvider.GetRequiredKeyedService>("cosmos"); +cosmosRepo.Save(new User { Id = 2, Name = "Jane" }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/040_KeyedService_MultipleKeys/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/040_KeyedService_MultipleKeys/Repository.cs new file mode 100644 index 0000000..3546e6d --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/040_KeyedService_MultipleKeys/Repository.cs @@ -0,0 +1,48 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +[DecoratedBy(typeof(CachingRepository<>))] +public class SqlRepository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[SQL] Saving {typeof(T).Name}..."); + } +} + +[DecoratedBy(typeof(CachingRepository<>))] +public class CosmosRepository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[Cosmos] Saving {typeof(T).Name}..."); + } +} + +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + + public CachingRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Cache] Checking cache for {typeof(T).Name}..."); + _inner.Save(entity); + } +} + +public class User +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/041_KeyedService_MultipleDecorators/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/041_KeyedService_MultipleDecorators/Program.cs new file mode 100644 index 0000000..d2e9a79 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/041_KeyedService_MultipleDecorators/Program.cs @@ -0,0 +1,11 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Keyed service with multiple decorators (should apply in ascending order) +services.AddKeyedScoped, DynamoDbRepository>("primary"); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredKeyedService>("primary"); +repo.Save(new Product { Id = 1, Name = "Widget" }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/041_KeyedService_MultipleDecorators/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/041_KeyedService_MultipleDecorators/Repository.cs new file mode 100644 index 0000000..0433ad4 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/041_KeyedService_MultipleDecorators/Repository.cs @@ -0,0 +1,56 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +[DecoratedBy(typeof(LoggingRepository<>), Order = 2)] +[DecoratedBy(typeof(CachingRepository<>), Order = 1)] +public class DynamoDbRepository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[DynamoDB] Saving {typeof(T).Name}..."); + } +} + +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + + public CachingRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Cache] Checking cache for {typeof(T).Name}..."); + _inner.Save(entity); + } +} + +public class LoggingRepository : IRepository +{ + private readonly IRepository _inner; + + public LoggingRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Log] Save called for {typeof(T).Name}"); + _inner.Save(entity); + } +} + +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/042_KeyedService_IntegerKey/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/042_KeyedService_IntegerKey/Program.cs new file mode 100644 index 0000000..4fc50a1 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/042_KeyedService_IntegerKey/Program.cs @@ -0,0 +1,11 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Keyed service with integer key (demonstrates non-string keys) +services.AddKeyedScoped, DatabaseRepository>(1); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredKeyedService>(1); +repo.Save(new Order { Id = 100, Total = 299.99m }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/042_KeyedService_IntegerKey/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/042_KeyedService_IntegerKey/Repository.cs new file mode 100644 index 0000000..84d9b22 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/042_KeyedService_IntegerKey/Repository.cs @@ -0,0 +1,39 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +[DecoratedBy(typeof(LoggingRepository<>))] +public class DatabaseRepository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[Database] Saving {typeof(T).Name}..."); + } +} + +public class LoggingRepository : IRepository +{ + private readonly IRepository _inner; + + public LoggingRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Log] Save called for {typeof(T).Name}"); + _inner.Save(entity); + } +} + +public class Order +{ + public int Id { get; set; } + public decimal Total { get; set; } +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/043_KeyedService_FactoryDelegate/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/043_KeyedService_FactoryDelegate/Program.cs new file mode 100644 index 0000000..c51c579 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/043_KeyedService_FactoryDelegate/Program.cs @@ -0,0 +1,14 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Keyed service with factory delegate - decorator should still be applied +services.AddKeyedScoped, ConfigurableRepository>( + "primary", + (sp, key) => new ConfigurableRepository("Server=primary;Database=Main") +); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredKeyedService>("primary"); +repo.Save(new Customer { Id = 1, Name = "Alice" }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/043_KeyedService_FactoryDelegate/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/043_KeyedService_FactoryDelegate/Repository.cs new file mode 100644 index 0000000..a7a92c9 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/043_KeyedService_FactoryDelegate/Repository.cs @@ -0,0 +1,46 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +[DecoratedBy(typeof(CachingRepository<>))] +public class ConfigurableRepository : IRepository +{ + private readonly string _connectionString; + + public ConfigurableRepository(string connectionString) + { + _connectionString = connectionString; + } + + public void Save(T entity) + { + Console.WriteLine($"[Configurable] Saving {typeof(T).Name} to {_connectionString}..."); + } +} + +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + + public CachingRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Cache] Checking cache for {typeof(T).Name}..."); + _inner.Save(entity); + } +} + +public class Customer +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/044_KeyedService_NoDecorators/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/044_KeyedService_NoDecorators/Program.cs new file mode 100644 index 0000000..6f78a9e --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/044_KeyedService_NoDecorators/Program.cs @@ -0,0 +1,11 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Keyed service without decorators - should pass through +services.AddKeyedScoped, PlainRepository>("cache"); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredKeyedService>("cache"); +repo.Save(new Data { Id = 42, Value = "Test" }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/044_KeyedService_NoDecorators/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/044_KeyedService_NoDecorators/Repository.cs new file mode 100644 index 0000000..4c7e3fb --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/044_KeyedService_NoDecorators/Repository.cs @@ -0,0 +1,21 @@ +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +// No decorators - should pass through to original method +public class PlainRepository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[Plain] Saving {typeof(T).Name}..."); + } +} + +public class Data +{ + public int Id { get; set; } + public string Value { get; set; } = string.Empty; +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/045_KeyedService_Transient/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/045_KeyedService_Transient/Program.cs new file mode 100644 index 0000000..da24228 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/045_KeyedService_Transient/Program.cs @@ -0,0 +1,11 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Keyed service with Transient lifetime +services.AddKeyedTransient, MemoryRepository>("events"); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredKeyedService>("events"); +repo.Save(new Event { Id = 1, Type = "UserCreated" }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/045_KeyedService_Transient/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/045_KeyedService_Transient/Repository.cs new file mode 100644 index 0000000..d3eb9c7 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/045_KeyedService_Transient/Repository.cs @@ -0,0 +1,39 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +[DecoratedBy(typeof(LoggingRepository<>))] +public class MemoryRepository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[Memory] Saving {typeof(T).Name}..."); + } +} + +public class LoggingRepository : IRepository +{ + private readonly IRepository _inner; + + public LoggingRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Log] Save called for {typeof(T).Name}"); + _inner.Save(entity); + } +} + +public class Event +{ + public int Id { get; set; } + public string Type { get; set; } = string.Empty; +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs index ec79e03..469a8a6 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs @@ -484,4 +484,95 @@ await VerifyGlue.VerifySourcesAsync(sut, ], featureFlags: FeatureFlags); } + + [Theory] + [GeneratorAutoData] + public async Task KeyedService_SingleDecorator(DecoWeaverGenerator sut) + { + // Test keyed service with single decorator + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/039_KeyedService_SingleDecorator/Repository.cs", + "Cases/039_KeyedService_SingleDecorator/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task KeyedService_MultipleKeys(DecoWeaverGenerator sut) + { + // Test multiple keyed services with same interface but different keys + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/040_KeyedService_MultipleKeys/Repository.cs", + "Cases/040_KeyedService_MultipleKeys/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task KeyedService_MultipleDecorators(DecoWeaverGenerator sut) + { + // Test keyed service with multiple decorators in ascending order + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/041_KeyedService_MultipleDecorators/Repository.cs", + "Cases/041_KeyedService_MultipleDecorators/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task KeyedService_IntegerKey(DecoWeaverGenerator sut) + { + // Test keyed service with integer key (non-string key type) + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/042_KeyedService_IntegerKey/Repository.cs", + "Cases/042_KeyedService_IntegerKey/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task KeyedService_FactoryDelegate(DecoWeaverGenerator sut) + { + // Test keyed service with factory delegate - decorator should still be applied + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/043_KeyedService_FactoryDelegate/Repository.cs", + "Cases/043_KeyedService_FactoryDelegate/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task KeyedService_NoDecorators(DecoWeaverGenerator sut) + { + // Test keyed service without decorators - should pass through + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/044_KeyedService_NoDecorators/Repository.cs", + "Cases/044_KeyedService_NoDecorators/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task KeyedService_Transient(DecoWeaverGenerator sut) + { + // Test keyed service with Transient lifetime + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/045_KeyedService_Transient/Repository.cs", + "Cases/045_KeyedService_Transient/Program.cs" + ], + featureFlags: FeatureFlags); + } } \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/LayeredCraft.DecoWeaver.Generator.Tests.csproj b/test/LayeredCraft.DecoWeaver.Generator.Tests/LayeredCraft.DecoWeaver.Generator.Tests.csproj index 16f3755..3625817 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/LayeredCraft.DecoWeaver.Generator.Tests.csproj +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/LayeredCraft.DecoWeaver.Generator.Tests.csproj @@ -30,12 +30,11 @@ - - + - - + + @@ -45,14 +44,17 @@ + + + diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_IsolationCheck_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_IsolationCheck_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 6b6522e..294ec9e 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_IsolationCheck_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_IsolationCheck_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_Multiple_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_Multiple_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 2a0c99c..4863230 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_Multiple_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_Multiple_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_RemovesAssemblyDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_RemovesAssemblyDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index e373fe2..f893901 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_RemovesAssemblyDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_RemovesAssemblyDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_ComplexDependencies_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_ComplexDependencies_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 843191b..1e3a413 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_ComplexDependencies_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_ComplexDependencies_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 420026d..836e9d7 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -50,6 +50,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index a1cbee8..2610541 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Singleton_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Singleton_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index e5db754..18017e8 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Singleton_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Singleton_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Transient_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Transient_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index e19617d..3e4da68 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Transient_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Transient_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_MixedSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_MixedSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 25db750..b253872 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_MixedSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_MixedSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -51,6 +51,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 75159c9..548d87d 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -50,6 +50,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 84e592f..99a344e 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_FactoryDelegate_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_FactoryDelegate_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..a1312da --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_FactoryDelegate_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,80 @@ +//HintName: DecoWeaver.Interceptors.ClosedGenerics.g.cs +// +#nullable enable + +namespace System.Runtime.CompilerServices +{ + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} + +namespace DecoWeaver.Generated +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Runtime.CompilerServices; + using Microsoft.Extensions.DependencyInjection; + + file static class DecoWeaverInterceptors + { + [InterceptsLocation(version: 1, data: "OaqdhK7LdFzUTOlxfshS6ccAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddKeyedScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.ConfigurableRepository>(IServiceCollection, object?, Func<IServiceProvider, object?, global::DecoWeaver.Sample.ConfigurableRepository>) + internal static IServiceCollection AddKeyedScoped_0(this IServiceCollection services, object? serviceKey, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + // Create nested key to avoid circular resolution + var nestedKey = DecoratorKeys.ForKeyed(serviceKey, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.ConfigurableRepository)); + + // Register the undecorated implementation with nested key and factory + services.AddKeyedScoped>(nestedKey, (sp, key) => (global::DecoWeaver.Sample.IRepository)implementationFactory(sp, serviceKey)); + + // Register factory with user's key that applies decorators + services.AddKeyedScoped>(serviceKey, (sp, key) => + { + var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(nestedKey)!; + // Compose decorators (innermost to outermost) + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.CachingRepository<>), current); + return current; + }); + return services; + } + + private static class DecoratorKeys + { + public static object For(Type serviceType, Type implementationType) + { + var s = serviceType.AssemblyQualifiedName ?? serviceType.FullName ?? serviceType.Name; + var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; + return string.Concat(s, "|", i); + } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } + } + + private static class DecoratorFactory + { + public static object Create(IServiceProvider sp, Type serviceType, Type decoratorOpenOrClosed, object inner) + { + var closedType = CloseIfNeeded(decoratorOpenOrClosed, serviceType); + return ActivatorUtilities.CreateInstance(sp, closedType, inner)!; + } + + private static Type CloseIfNeeded(Type t, Type serviceType) + { + if (!t.IsGenericTypeDefinition) return t; + var args = serviceType.IsGenericType ? serviceType.GetGenericArguments() : Type.EmptyTypes; + return t.MakeGenericType(args); + } + } + + } +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_IntegerKey_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_IntegerKey_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..0c1e398 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_IntegerKey_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,80 @@ +//HintName: DecoWeaver.Interceptors.ClosedGenerics.g.cs +// +#nullable enable + +namespace System.Runtime.CompilerServices +{ + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} + +namespace DecoWeaver.Generated +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Runtime.CompilerServices; + using Microsoft.Extensions.DependencyInjection; + + file static class DecoWeaverInterceptors + { + [InterceptsLocation(version: 1, data: "3dCHvh0AORJGCY7Ahl5rOr0AAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddKeyedScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DatabaseRepository>(IServiceCollection, object?) + internal static IServiceCollection AddKeyedScoped_0(this IServiceCollection services, object? serviceKey) + where TService : class + where TImplementation : class, TService + { + // Create nested key to avoid circular resolution + var nestedKey = DecoratorKeys.ForKeyed(serviceKey, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DatabaseRepository)); + + // Register the undecorated implementation with nested key + services.AddKeyedScoped, global::DecoWeaver.Sample.DatabaseRepository>(nestedKey); + + // Register factory with user's key that applies decorators + services.AddKeyedScoped>(serviceKey, (sp, key) => + { + var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(nestedKey)!; + // Compose decorators (innermost to outermost) + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.LoggingRepository<>), current); + return current; + }); + return services; + } + + private static class DecoratorKeys + { + public static object For(Type serviceType, Type implementationType) + { + var s = serviceType.AssemblyQualifiedName ?? serviceType.FullName ?? serviceType.Name; + var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; + return string.Concat(s, "|", i); + } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } + } + + private static class DecoratorFactory + { + public static object Create(IServiceProvider sp, Type serviceType, Type decoratorOpenOrClosed, object inner) + { + var closedType = CloseIfNeeded(decoratorOpenOrClosed, serviceType); + return ActivatorUtilities.CreateInstance(sp, closedType, inner)!; + } + + private static Type CloseIfNeeded(Type t, Type serviceType) + { + if (!t.IsGenericTypeDefinition) return t; + var args = serviceType.IsGenericType ? serviceType.GetGenericArguments() : Type.EmptyTypes; + return t.MakeGenericType(args); + } + } + + } +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..5260a51 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,81 @@ +//HintName: DecoWeaver.Interceptors.ClosedGenerics.g.cs +// +#nullable enable + +namespace System.Runtime.CompilerServices +{ + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} + +namespace DecoWeaver.Generated +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Runtime.CompilerServices; + using Microsoft.Extensions.DependencyInjection; + + file static class DecoWeaverInterceptors + { + [InterceptsLocation(version: 1, data: "O6Fzvz/Vg7lk4dQtJmvKi8gAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddKeyedScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection, object?) + internal static IServiceCollection AddKeyedScoped_0(this IServiceCollection services, object? serviceKey) + where TService : class + where TImplementation : class, TService + { + // Create nested key to avoid circular resolution + var nestedKey = DecoratorKeys.ForKeyed(serviceKey, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + + // Register the undecorated implementation with nested key + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(nestedKey); + + // Register factory with user's key that applies decorators + services.AddKeyedScoped>(serviceKey, (sp, key) => + { + var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(nestedKey)!; + // Compose decorators (innermost to outermost) + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.CachingRepository<>), current); + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.LoggingRepository<>), current); + return current; + }); + return services; + } + + private static class DecoratorKeys + { + public static object For(Type serviceType, Type implementationType) + { + var s = serviceType.AssemblyQualifiedName ?? serviceType.FullName ?? serviceType.Name; + var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; + return string.Concat(s, "|", i); + } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } + } + + private static class DecoratorFactory + { + public static object Create(IServiceProvider sp, Type serviceType, Type decoratorOpenOrClosed, object inner) + { + var closedType = CloseIfNeeded(decoratorOpenOrClosed, serviceType); + return ActivatorUtilities.CreateInstance(sp, closedType, inner)!; + } + + private static Type CloseIfNeeded(Type t, Type serviceType) + { + if (!t.IsGenericTypeDefinition) return t; + var args = serviceType.IsGenericType ? serviceType.GetGenericArguments() : Type.EmptyTypes; + return t.MakeGenericType(args); + } + } + + } +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_MultipleKeys_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_MultipleKeys_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..d09029c --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_MultipleKeys_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,103 @@ +//HintName: DecoWeaver.Interceptors.ClosedGenerics.g.cs +// +#nullable enable + +namespace System.Runtime.CompilerServices +{ + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} + +namespace DecoWeaver.Generated +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Runtime.CompilerServices; + using Microsoft.Extensions.DependencyInjection; + + file static class DecoWeaverInterceptors + { + [InterceptsLocation(version: 1, data: "fAbqH9Sp6B/0qH0FYFTTab4AAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddKeyedScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.SqlRepository>(IServiceCollection, object?) + internal static IServiceCollection AddKeyedScoped_0(this IServiceCollection services, object? serviceKey) + where TService : class + where TImplementation : class, TService + { + // Create nested key to avoid circular resolution + var nestedKey = DecoratorKeys.ForKeyed(serviceKey, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.SqlRepository)); + + // Register the undecorated implementation with nested key + services.AddKeyedScoped, global::DecoWeaver.Sample.SqlRepository>(nestedKey); + + // Register factory with user's key that applies decorators + services.AddKeyedScoped>(serviceKey, (sp, key) => + { + var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(nestedKey)!; + // Compose decorators (innermost to outermost) + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.CachingRepository<>), current); + return current; + }); + return services; + } + + [InterceptsLocation(version: 1, data: "fAbqH9Sp6B/0qH0FYFTTaQYBAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddKeyedScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.CosmosRepository>(IServiceCollection, object?) + internal static IServiceCollection AddKeyedScoped_1(this IServiceCollection services, object? serviceKey) + where TService : class + where TImplementation : class, TService + { + // Create nested key to avoid circular resolution + var nestedKey = DecoratorKeys.ForKeyed(serviceKey, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.CosmosRepository)); + + // Register the undecorated implementation with nested key + services.AddKeyedScoped, global::DecoWeaver.Sample.CosmosRepository>(nestedKey); + + // Register factory with user's key that applies decorators + services.AddKeyedScoped>(serviceKey, (sp, key) => + { + var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(nestedKey)!; + // Compose decorators (innermost to outermost) + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.CachingRepository<>), current); + return current; + }); + return services; + } + + private static class DecoratorKeys + { + public static object For(Type serviceType, Type implementationType) + { + var s = serviceType.AssemblyQualifiedName ?? serviceType.FullName ?? serviceType.Name; + var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; + return string.Concat(s, "|", i); + } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } + } + + private static class DecoratorFactory + { + public static object Create(IServiceProvider sp, Type serviceType, Type decoratorOpenOrClosed, object inner) + { + var closedType = CloseIfNeeded(decoratorOpenOrClosed, serviceType); + return ActivatorUtilities.CreateInstance(sp, closedType, inner)!; + } + + private static Type CloseIfNeeded(Type t, Type serviceType) + { + if (!t.IsGenericTypeDefinition) return t; + var args = serviceType.IsGenericType ? serviceType.GetGenericArguments() : Type.EmptyTypes; + return t.MakeGenericType(args); + } + } + + } +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..2451652 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,80 @@ +//HintName: DecoWeaver.Interceptors.ClosedGenerics.g.cs +// +#nullable enable + +namespace System.Runtime.CompilerServices +{ + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} + +namespace DecoWeaver.Generated +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Runtime.CompilerServices; + using Microsoft.Extensions.DependencyInjection; + + file static class DecoWeaverInterceptors + { + [InterceptsLocation(version: 1, data: "ITmZeNyhR+doREcOZVxSfrAAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddKeyedScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.SqlRepository>(IServiceCollection, object?) + internal static IServiceCollection AddKeyedScoped_0(this IServiceCollection services, object? serviceKey) + where TService : class + where TImplementation : class, TService + { + // Create nested key to avoid circular resolution + var nestedKey = DecoratorKeys.ForKeyed(serviceKey, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.SqlRepository)); + + // Register the undecorated implementation with nested key + services.AddKeyedScoped, global::DecoWeaver.Sample.SqlRepository>(nestedKey); + + // Register factory with user's key that applies decorators + services.AddKeyedScoped>(serviceKey, (sp, key) => + { + var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(nestedKey)!; + // Compose decorators (innermost to outermost) + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.LoggingRepository<>), current); + return current; + }); + return services; + } + + private static class DecoratorKeys + { + public static object For(Type serviceType, Type implementationType) + { + var s = serviceType.AssemblyQualifiedName ?? serviceType.FullName ?? serviceType.Name; + var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; + return string.Concat(s, "|", i); + } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } + } + + private static class DecoratorFactory + { + public static object Create(IServiceProvider sp, Type serviceType, Type decoratorOpenOrClosed, object inner) + { + var closedType = CloseIfNeeded(decoratorOpenOrClosed, serviceType); + return ActivatorUtilities.CreateInstance(sp, closedType, inner)!; + } + + private static Type CloseIfNeeded(Type t, Type serviceType) + { + if (!t.IsGenericTypeDefinition) return t; + var args = serviceType.IsGenericType ? serviceType.GetGenericArguments() : Type.EmptyTypes; + return t.MakeGenericType(args); + } + } + + } +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_Transient_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_Transient_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..7816831 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedService_Transient_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,80 @@ +//HintName: DecoWeaver.Interceptors.ClosedGenerics.g.cs +// +#nullable enable + +namespace System.Runtime.CompilerServices +{ + file sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +} + +namespace DecoWeaver.Generated +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Runtime.CompilerServices; + using Microsoft.Extensions.DependencyInjection; + + file static class DecoWeaverInterceptors + { + [InterceptsLocation(version: 1, data: "fu9FYAsyQKsaGQQklfC9j6UAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddKeyedTransient<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.MemoryRepository>(IServiceCollection, object?) + internal static IServiceCollection AddKeyedTransient_0(this IServiceCollection services, object? serviceKey) + where TService : class + where TImplementation : class, TService + { + // Create nested key to avoid circular resolution + var nestedKey = DecoratorKeys.ForKeyed(serviceKey, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.MemoryRepository)); + + // Register the undecorated implementation with nested key + services.AddKeyedTransient, global::DecoWeaver.Sample.MemoryRepository>(nestedKey); + + // Register factory with user's key that applies decorators + services.AddKeyedTransient>(serviceKey, (sp, key) => + { + var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(nestedKey)!; + // Compose decorators (innermost to outermost) + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.LoggingRepository<>), current); + return current; + }); + return services; + } + + private static class DecoratorKeys + { + public static object For(Type serviceType, Type implementationType) + { + var s = serviceType.AssemblyQualifiedName ?? serviceType.FullName ?? serviceType.Name; + var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; + return string.Concat(s, "|", i); + } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } + } + + private static class DecoratorFactory + { + public static object Create(IServiceProvider sp, Type serviceType, Type decoratorOpenOrClosed, object inner) + { + var closedType = CloseIfNeeded(decoratorOpenOrClosed, serviceType); + return ActivatorUtilities.CreateInstance(sp, closedType, inner)!; + } + + private static Type CloseIfNeeded(Type t, Type serviceType) + { + if (!t.IsGenericTypeDefinition) return t; + var args = serviceType.IsGenericType ? serviceType.GetGenericArguments() : Type.EmptyTypes; + return t.MakeGenericType(args); + } + } + + } +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_Deduplication_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_Deduplication_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 22edee4..3085313 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_Deduplication_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_Deduplication_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -72,6 +72,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_SortOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_SortOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 60aa028..ee578e2 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_SortOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_SortOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -50,6 +50,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ConstructorOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ConstructorOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 6765a7a..e48b8e7 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ConstructorOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ConstructorOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MixedOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MixedOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 02c048e..0ce1af2 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MixedOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MixedOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -50,6 +50,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleDefaultOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleDefaultOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index dc96a5e..65f1318 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleDefaultOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleDefaultOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -50,6 +50,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 0231a42..a1f746f 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -72,6 +72,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_NonGenericDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_NonGenericDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 83caa20..012f3d9 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_NonGenericDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_NonGenericDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 2d81b68..536e6f8 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -70,6 +70,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ThreeDecorators_GeneratesCorrectNesting_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ThreeDecorators_GeneratesCorrectNesting_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index e44c2e3..29773e5 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ThreeDecorators_GeneratesCorrectNesting_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ThreeDecorators_GeneratesCorrectNesting_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -51,6 +51,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 0231a42..a1f746f 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -72,6 +72,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 2d81b68..536e6f8 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -70,6 +70,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SkipAssemblyWithClassLevel_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SkipAssemblyWithClassLevel_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index b907efe..7e87dc3 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SkipAssemblyWithClassLevel_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SkipAssemblyWithClassLevel_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_WithSkipAssembly_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_WithSkipAssembly_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 8dcb1a5..f754f95 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_WithSkipAssembly_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_WithSkipAssembly_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_Generic_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_Generic_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index aefc63d..fb86d13 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_Generic_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_Generic_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -50,6 +50,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 1b2c820..28a09de 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index a356d52..64deb9a 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -50,6 +50,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 24949a2..5550b37 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_Generic_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_Generic_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 1e5a3fe..dfb203f 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_Generic_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_Generic_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -50,6 +50,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index fcc52e9..4967fc6 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_Generic_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 142fe0f..22f4ff1 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -50,6 +50,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 96a23d7..c8c4bb9 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -49,6 +49,13 @@ public static object For(Type serviceType, Type implementationType) var i = implementationType.AssemblyQualifiedName ?? implementationType.FullName ?? implementationType.Name; return string.Concat(s, "|", i); } + + public static object ForKeyed(object? userKey, Type serviceType, Type implementationType) + { + // Return a tuple that preserves the actual key object (not its string representation) + // This ensures distinct object keys create distinct nested keys + return (userKey, serviceType, implementationType); + } } private static class DecoratorFactory