diff --git a/CLAUDE.md b/CLAUDE.md index 0b0cac1..8d1747d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -189,6 +189,9 @@ services.AddScoped>(sp => new Repository()); services.AddKeyedScoped, Repository>("sql"); services.AddKeyedScoped, Repository>("sql", (sp, key) => new Repository()); +// ✅ Instance registration (singleton only) - INTERCEPTED by DecoWeaver (v1.0.4+) +services.AddSingleton>(new Repository()); + // ❌ Open generic registration - NOT intercepted services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); ``` @@ -274,6 +277,49 @@ var sqlRepo = serviceProvider.GetRequiredKeyedService>("sql"); - Multiple keys for same service type work independently - Each keyed registration is intercepted separately +### Instance Registration Support (v1.0.4+) + +DecoWeaver supports singleton instance registrations. This allows decorators to be applied to pre-created instances. + +**Instance registration**: +```csharp +// ✅ Supported - AddSingleton with instance +var instance = new SqlRepository(); +services.AddSingleton>(instance); + +// ❌ NOT supported - AddScoped/AddTransient don't have instance overloads in .NET DI +services.AddScoped>(instance); // Compiler error +services.AddTransient>(instance); // Compiler error +``` + +**How it works**: +- Instance type is extracted from the actual argument expression using `SemanticModel.GetTypeInfo(instanceArg).Type` +- Instance is registered directly as a keyed service (preserves disposal semantics) +- Decorators are applied around the instance just like other registration types + +**Generated code example**: +```csharp +// User code: +services.AddSingleton>(new SqlRepository()); + +// What DecoWeaver generates: +var key = DecoratorKeys.For(typeof(IRepository), typeof(SqlRepository)); +var capturedInstance = (IRepository)(object)implementationInstance; +services.AddKeyedSingleton>(key, capturedInstance); + +services.AddSingleton>(sp => +{ + var current = sp.GetRequiredKeyedService>(key); + current = (IRepository)DecoratorFactory.Create(sp, typeof(IRepository), typeof(LoggingRepository<>), current); + return current; +}); +``` + +**Limitations**: +- Only `AddSingleton` is supported (instance registrations don't exist for Scoped/Transient in .NET DI) +- The instance must be created before registration (can't use DI for instance construction) +- All resolutions return decorators wrapping the same singleton instance + ### Attribute Compilation - Attributes marked with `[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")]` diff --git a/Directory.Build.props b/Directory.Build.props index 389c18a..aadab28 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ - 1.0.3-beta + 1.0.4-beta MIT diff --git a/LayeredCraft.DecoWeaver.slnx b/LayeredCraft.DecoWeaver.slnx index 1f254db..1e4b5ce 100644 --- a/LayeredCraft.DecoWeaver.slnx +++ b/LayeredCraft.DecoWeaver.slnx @@ -56,7 +56,7 @@ - + @@ -64,10 +64,10 @@ - + - + \ No newline at end of file diff --git a/README.md b/README.md index f891c93..f1f1705 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ For more examples including open generics, multiple decorators, and ordering, se - **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(...))` +- **Instance Registration Support**: Works with singleton instances like `AddSingleton(instance)` - **Opt-Out Support**: Exclude specific decorators with `[DoNotDecorate]` - **Multiple Decorators**: Stack multiple decorators with explicit ordering - **Generic Type Decoration**: Decorate generic types like `IRepository` with open generic decorators diff --git a/docs/advanced/instance-registrations.md b/docs/advanced/instance-registrations.md new file mode 100644 index 0000000..2065f92 --- /dev/null +++ b/docs/advanced/instance-registrations.md @@ -0,0 +1,233 @@ +# Instance Registrations + +DecoWeaver supports decorating singleton instance registrations starting with version 1.0.4-beta. This allows you to apply decorators to pre-configured instances that you register directly with the DI container. + +## Overview + +Instance registrations let you register a pre-created instance directly with the DI container. DecoWeaver can intercept these registrations and apply decorators around your instance, just like it does for parameterless and factory delegate registrations. + +## Supported Patterns + +### Single Type Parameter with Instance + +```csharp +// Register a pre-created instance +var instance = new SqlRepository(); +services.AddSingleton>(instance); + +// DecoWeaver will apply decorators around the instance +var repo = serviceProvider.GetRequiredService>(); +// Returns: LoggingRepository wrapping SqlRepository instance +``` + +### Keyed Instance Registration + +```csharp +// Register a pre-created instance with a key +var instance = new SqlRepository(); +services.AddKeyedSingleton>("primary", instance); + +// Resolve using the same key +var repo = serviceProvider.GetRequiredKeyedService>("primary"); +// Returns: LoggingRepository wrapping SqlRepository instance +``` + +## Limitations + +### Singleton Only + +Instance registrations are **only supported with `AddSingleton`**. This is a limitation of .NET's dependency injection framework itself: + +```csharp +// ✅ Supported - AddSingleton with instance +services.AddSingleton>(instance); + +// ❌ NOT supported - AddScoped doesn't have instance overload in .NET DI +services.AddScoped>(instance); // Compiler error + +// ❌ NOT supported - AddTransient doesn't have instance overload in .NET DI +services.AddTransient>(instance); // Compiler error +``` + +The reason is that scoped and transient lifetimes are incompatible with instance registrations - they require creating new instances on each resolution or scope, which contradicts the concept of registering a pre-created instance. + +## How It Works + +When DecoWeaver encounters an instance registration: + +1. **Instance Type Extraction**: The generator extracts the actual type of the instance from the argument expression + ```csharp + // User code: + services.AddSingleton>(new SqlRepository()); + + // DecoWeaver sees: + // - Service type: IRepository + // - Implementation type: SqlRepository (extracted from "new SqlRepository()") + ``` + +2. **Keyed Service Registration**: The instance is registered directly as a keyed service + ```csharp + // Generated code: + var key = DecoratorKeys.For(typeof(IRepository), typeof(SqlRepository)); + var capturedInstance = (IRepository)(object)implementationInstance; + services.AddKeyedSingleton>(key, capturedInstance); + ``` + +3. **Decorator Application**: Decorators are applied around the keyed service + ```csharp + // Generated code: + services.AddSingleton>(sp => + { + var current = sp.GetRequiredKeyedService>(key); + current = (IRepository)DecoratorFactory.Create( + sp, typeof(IRepository), typeof(LoggingRepository<>), current); + return current; + }); + ``` + +## Examples + +### Basic Instance Registration + +```csharp +[DecoratedBy>] +public class SqlRepository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[SQL] Saving {typeof(T).Name}..."); + } +} + +// Register pre-created instance +var instance = new SqlRepository(); +services.AddSingleton>(instance); + +// The same instance is reused for all resolutions, but wrapped with decorators +var repo1 = serviceProvider.GetRequiredService>(); +var repo2 = serviceProvider.GetRequiredService>(); +// repo1 and repo2 both wrap the same SqlRepository instance +``` + +### Multiple Decorators with Instance + +```csharp +[DecoratedBy>(Order = 1)] +[DecoratedBy>(Order = 2)] +public class SqlRepository : IRepository { /* ... */ } + +var instance = new SqlRepository(); +services.AddSingleton>(instance); + +// Resolved as: LoggingRepository wrapping CachingRepository wrapping instance +``` + +### Pre-Configured Instance + +```csharp +// Useful when instance needs complex initialization +var connectionString = configuration.GetConnectionString("Production"); +var instance = new SqlRepository(connectionString) +{ + CommandTimeout = TimeSpan.FromSeconds(30), + EnableRetries = true +}; + +services.AddSingleton>(instance); +// Decorators are applied, but the pre-configured instance is preserved +``` + +### Keyed Instance with Multiple Configurations + +```csharp +[DecoratedBy>] +public class SqlRepository : IRepository { /* ... */ } + +// Register multiple instances with different configurations +var primaryDb = new SqlRepository("Server=primary;Database=Main"); +var secondaryDb = new SqlRepository("Server=secondary;Database=Replica"); + +services.AddKeyedSingleton>("primary", primaryDb); +services.AddKeyedSingleton>("secondary", secondaryDb); + +// Each key resolves its own instance with decorators applied +var primary = serviceProvider.GetRequiredKeyedService>("primary"); +var secondary = serviceProvider.GetRequiredKeyedService>("secondary"); +// Both are wrapped with LoggingRepository, but use different SqlRepository instances +``` + +## Technical Details + +### Type Extraction from Arguments + +DecoWeaver uses Roslyn's semantic model to extract the actual type from the instance argument: + +```csharp +// In ClosedGenericRegistrationProvider.cs - Non-keyed instance +var args = inv.ArgumentList.Arguments; +if (args.Count >= 1) +{ + var instanceArg = args[0].Expression; // Extension methods don't include 'this' in ArgumentList + var instanceType = semanticModel.GetTypeInfo(instanceArg).Type as INamedTypeSymbol; + return (serviceType, instanceType); // e.g., (IRepository, SqlRepository) +} + +// For keyed instances +if (args.Count >= 2) // Key parameter + instance parameter +{ + var instanceArg = args[1].Expression; // Second argument after the key + var instanceType = semanticModel.GetTypeInfo(instanceArg).Type as INamedTypeSymbol; + return (serviceType, instanceType); +} +``` + +### Direct Instance Registration + +DecoWeaver uses the direct instance overload available in .NET DI for keyed singleton services: + +```csharp +// DecoWeaver generates: +var key = DecoratorKeys.For(typeof(IRepository), typeof(SqlRepository)); +var capturedInstance = (IRepository)(object)implementationInstance; +services.AddKeyedSingleton>(key, capturedInstance); +``` + +This preserves the expected .NET DI disposal semantics - the container owns and disposes the instance when the container is disposed, just like non-keyed singleton instance registrations. + +The double cast `(TService)(object)` ensures the generic type parameter `TService` is compatible with the captured instance. + +## When to Use Instance Registrations + +Instance registrations with DecoWeaver are useful when: + +1. **Pre-configured Dependencies**: Your instance needs complex initialization that's easier to do outside of DI +2. **External Resources**: Registering wrappers around external resources (e.g., database connections, message queues) +3. **Testing/Mocking**: Registering test doubles or mocks with specific configurations +4. **Singleton State**: When you need a true singleton with decorators applied + +## Alternatives + +If you need more flexibility, consider these alternatives: + +### Factory Delegates +```csharp +// More flexible than instances - can use IServiceProvider +services.AddSingleton>(sp => +{ + var config = sp.GetRequiredService(); + return new SqlRepository(config.GetConnectionString("Default")); +}); +``` + +### Parameterless with Constructor Injection +```csharp +// Let DI handle the construction +services.AddSingleton, SqlRepository>(); +// SqlRepository constructor receives dependencies from DI +``` + +## See Also + +- [Factory Delegates](../usage/factory-delegates.md) - Using factory functions with decorators +- [Keyed Services](keyed-services.md) - How DecoWeaver uses keyed services internally +- [How It Works](../core-concepts/how-it-works.md) - Understanding the generation process diff --git a/docs/changelog.md b/docs/changelog.md index 09bdc3f..7a55425 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - No changes yet +## [1.0.4-beta] - 2025-11-13 + +### Added +- **Instance registration support** - Decorators now work with singleton instance registrations + - `AddSingleton(instance)` - Single type parameter with instance + - Decorators are applied around the provided instance + - Only `AddSingleton` is supported (instance registrations don't exist for Scoped/Transient in .NET DI) + +### Changed +- Extended `RegistrationKind` enum with `InstanceSingleTypeParam` variant +- Added `InstanceParameterName` field to `ClosedGenericRegistration` model +- Updated `ClosedGenericRegistrationProvider` to detect instance registrations (non-delegate second parameter) +- Updated `InterceptorEmitter` to generate instance interceptors with factory lambda wrapping +- Instance type is extracted from the actual argument expression (e.g., `new SqlRepository()`) +- Instances are registered as keyed services via factory lambda (keyed services don't have instance overloads) + +### Technical Details +- Instance detection: parameter type must match type parameter and NOT be a `Func<>` delegate +- Only `AddSingleton` accepted - `AddScoped`/`AddTransient` don't support instance parameters in .NET DI +- Instance wrapped in factory lambda: `services.AddKeyedSingleton(key, (sp, _) => capturedInstance)` +- Type extraction uses `SemanticModel.GetTypeInfo(instanceArg).Type` to get actual implementation type +- Extension method ArgumentList doesn't include `this` parameter, so instance is at `args[0]` +- 3 new test cases (047-049) covering instance registration scenarios +- Updated sample project with instance registration example +- All existing functionality remains unchanged - this is purely additive + ## [1.0.3-beta] - 2025-11-13 ### Added diff --git a/releasenotes.props b/releasenotes.props index 63dc89d..0ff388a 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -147,6 +147,47 @@ This release adds support for keyed service registrations, completing issue #3 ( * Changelog: https://layeredcraft.github.io/decoweaver/changelog/ * GitHub: https://github.com/layeredcraft/decoweaver +]]> + + + + (instance)` - Single type parameter with instance + - Decorators are applied around the provided instance + - Only `AddSingleton` is supported (instance registrations don't exist for Scoped/Transient in .NET DI) + +### What's Changed + +* Extended `RegistrationKind` enum with `InstanceSingleTypeParam` variant +* Added `InstanceParameterName` field to `ClosedGenericRegistration` model +* Updated `ClosedGenericRegistrationProvider` to detect instance registrations (non-delegate second parameter) +* Updated `InterceptorEmitter` to generate instance interceptors with factory lambda wrapping +* Instance type is extracted from the actual argument expression (e.g., `new SqlRepository()`) +* Instances are registered as keyed services via factory lambda (keyed services don't have instance overloads) + +### Technical Details + +* Instance detection: parameter type must match type parameter and NOT be a `Func<>` delegate +* Only `AddSingleton` accepted - `AddScoped`/`AddTransient` don't support instance parameters in .NET DI +* Instance wrapped in factory lambda: `services.AddKeyedSingleton(key, (sp, _) => capturedInstance)` +* Type extraction uses `SemanticModel.GetTypeInfo(instanceArg).Type` to get actual implementation type +* 3 new test cases (047-049) covering instance registration scenarios +* Updated sample project with instance registration example +* 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 cfb4633..27bcb14 100644 --- a/samples/DecoWeaver.Sample/Program.cs +++ b/samples/DecoWeaver.Sample/Program.cs @@ -26,6 +26,10 @@ // Example 6: Multiple keyed services with different keys .AddKeyedScoped, ConcreteClass>("cache") .AddKeyedScoped, ConcreteClass>("database") + // Example 7: Instance registration (singleton only) + .AddSingleton>(new DynamoDbRepository()) + // Example 8: Keyed instance registration + .AddKeyedSingleton>("keyed-instance", new DynamoDbRepository()) .BuildServiceProvider(); // Test Example 1: Open generic repository (parameterless) @@ -73,6 +77,20 @@ var databaseService = serviceProvider.GetRequiredKeyedService>("database"); Console.WriteLine($"Resolved 'database': {databaseService.GetType().Name}"); databaseService.DoSomething(100); +Console.WriteLine(); + +// Test Example 7: Instance registration +Console.WriteLine("=== Example 7: Instance Registration (Singleton) ==="); +var invoiceRepo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {invoiceRepo.GetType().Name}"); +invoiceRepo.Save(new Invoice { Id = 1, Amount = 1500.00m }); +Console.WriteLine(); + +// Test Example 8: Keyed instance registration +Console.WriteLine("=== Example 8: Keyed Instance Registration ==="); +var receiptRepo = serviceProvider.GetRequiredKeyedService>("keyed-instance"); +Console.WriteLine($"Resolved: {receiptRepo.GetType().Name}"); +receiptRepo.Save(new Receipt { Id = 1, Total = 250.00m }); public class Customer { @@ -81,6 +99,18 @@ public class Customer } public class Order +{ + public int Id { get; set; } + public decimal Total { get; set; } +} + +public class Invoice +{ + public int Id { get; set; } + public decimal Amount { get; set; } +} + +public class Receipt { public int Id { get; set; } public decimal Total { get; set; } diff --git a/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs b/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs index 0355fe1..882bdd5 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs @@ -114,6 +114,14 @@ private static void EmitSingleInterceptor( EmitKeyedFactorySingleTypeParamInterceptor(sb, methodName, reg, methodIndex, serviceFqn, implFqn, decorators); break; + case RegistrationKind.InstanceSingleTypeParam: + EmitInstanceSingleTypeParamInterceptor(sb, methodName, reg, methodIndex, serviceFqn, implFqn, decorators); + break; + + case RegistrationKind.KeyedInstanceSingleTypeParam: + EmitKeyedInstanceSingleTypeParamInterceptor(sb, methodName, reg, methodIndex, serviceFqn, implFqn, decorators); + break; + default: throw new InvalidOperationException($"Unsupported registration kind: {reg.Kind}"); } @@ -392,6 +400,99 @@ private static void EmitKeyedFactorySingleTypeParamInterceptor( sb.AppendLine(); } + private static void EmitInstanceSingleTypeParamInterceptor( + StringBuilder sb, + string methodName, + ClosedGenericRegistration reg, + int methodIndex, + string serviceFqn, + string implFqn, + string[] decorators) + { + var instanceParamName = reg.InstanceParameterName ?? "implementationInstance"; + + // Emit the method signature matching AddSingleton(services, T instance) + sb.AppendLine($" /// Intercepted: ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}>(IServiceCollection, {serviceFqn})"); + sb.AppendLine($" internal static IServiceCollection {methodName}_{methodIndex}(this IServiceCollection services, TService {instanceParamName})"); + sb.AppendLine(" where TService : class"); + sb.AppendLine(" {"); + + if (decorators.Length > 0) + { + sb.AppendLine(" // Register the undecorated instance as a keyed service"); + sb.AppendLine($" var key = DecoratorKeys.For(typeof({serviceFqn}), typeof({implFqn}));"); + sb.AppendLine($" var capturedInstance = ({serviceFqn})(object){instanceParamName};"); + sb.AppendLine($" services.{AddKeyed(methodName)}<{serviceFqn}>(key, capturedInstance);"); + sb.AppendLine(); + sb.AppendLine(" // Register factory that applies decorators around the instance"); + sb.AppendLine($" services.{methodName}<{serviceFqn}>(sp =>"); + sb.AppendLine(" {"); + sb.AppendLine($" var current = ({serviceFqn})sp.GetRequiredKeyedService<{serviceFqn}>(key)!;"); + 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 instance"); + sb.AppendLine($" return Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}>(services, {instanceParamName});"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } + + private static void EmitKeyedInstanceSingleTypeParamInterceptor( + StringBuilder sb, + string methodName, + ClosedGenericRegistration reg, + int methodIndex, + string serviceFqn, + string implFqn, + string[] decorators) + { + var serviceKeyParamName = reg.ServiceKeyParameterName ?? "serviceKey"; + var instanceParamName = reg.InstanceParameterName ?? "implementationInstance"; + + // Emit the method signature matching AddKeyedSingleton(services, object? serviceKey, T instance) + sb.AppendLine($" /// Intercepted: ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}>(IServiceCollection, object?, {serviceFqn})"); + sb.AppendLine($" internal static IServiceCollection {methodName}_{methodIndex}(this IServiceCollection services, object? {serviceKeyParamName}, TService {instanceParamName})"); + 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($" var capturedInstance = ({serviceFqn})(object){instanceParamName};"); + sb.AppendLine(); + sb.AppendLine(" // Register the undecorated instance with nested key"); + sb.AppendLine($" services.{methodName}<{serviceFqn}>(nestedKey, capturedInstance);"); + sb.AppendLine(); + sb.AppendLine(" // Register factory with user's key that applies decorators around the instance"); + 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 instance"); + sb.AppendLine($" return Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}>(services, {serviceKeyParamName}, {instanceParamName});"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } + private static string AddKeyed(string lifetimeMethod) => lifetimeMethod switch { diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs index b122bea..9f3b5ae 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs @@ -18,7 +18,11 @@ internal enum RegistrationKind /// 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 + KeyedFactorySingleTypeParam, + /// Instance with single type param: AddSingleton<T>(T implementationInstance) + InstanceSingleTypeParam, + /// Keyed instance with single type param: AddKeyedSingleton<T>(object? serviceKey, T implementationInstance) + KeyedInstanceSingleTypeParam } internal readonly record struct ClosedGenericRegistration( @@ -30,13 +34,15 @@ internal readonly record struct ClosedGenericRegistration( string InterceptsData, // "file|start|length" RegistrationKind Kind = RegistrationKind.Parameterless, string? FactoryParameterName = null, // Parameter name from the original registration (e.g., "implementationFactory") - string? ServiceKeyParameterName = null // Parameter name for keyed services (e.g., "serviceKey") + string? ServiceKeyParameterName = null, // Parameter name for keyed services (e.g., "serviceKey") + string? InstanceParameterName = null // Parameter name for instance registrations (e.g., "implementationInstance") ); internal readonly record struct RegistrationValidationResult( RegistrationKind Kind, string? FactoryParameterName, - string? ServiceKeyParameterName + string? ServiceKeyParameterName, + string? InstanceParameterName ); /// @@ -58,8 +64,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) { if (ctx.Node is not InvocationExpressionSyntax inv) return null; - var symbol = ctx.SemanticModel.GetSymbolInfo(inv).Symbol as IMethodSymbol; - if (symbol is null) return null; + if (ctx.SemanticModel.GetSymbolInfo(inv).Symbol is not IMethodSymbol symbol) return null; // For extension methods, we want the unreduced (static) form to check the containing type var symbolToCheck = symbol.ReducedFrom ?? symbol; @@ -86,7 +91,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) if (validationResult is null) return null; // Extract service and implementation types - var (svc, impl) = ExtractServiceAndImplementationTypes(symbol); + var (svc, impl) = ExtractServiceAndImplementationTypes(symbol, validationResult.Value.Kind, inv, ctx.SemanticModel); if (svc is null || impl is null) return null; // Get interceptable location @@ -106,7 +111,8 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) InterceptsData: interceptsData, Kind: validationResult.Value.Kind, FactoryParameterName: validationResult.Value.FactoryParameterName, - ServiceKeyParameterName: validationResult.Value.ServiceKeyParameterName + ServiceKeyParameterName: validationResult.Value.ServiceKeyParameterName, + InstanceParameterName: validationResult.Value.InstanceParameterName ); } @@ -125,15 +131,42 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) { // Parameterless registration: AddScoped() if (symbol.TypeArguments.Length != 2) return null; - return new RegistrationValidationResult(RegistrationKind.Parameterless, null, null); + return new RegistrationValidationResult(RegistrationKind.Parameterless, null, null, null); } - // Factory delegate registration (paramCount == 2) - var factoryParam = symbolToCheck.Parameters[1]; - if (!ValidateFactoryDelegate(factoryParam, expectedArgCount: 2, out var funcReturnType)) + // paramCount == 2 - could be factory delegate OR instance registration + var param2 = symbolToCheck.Parameters[1]; + + // Check if it's an instance parameter (not a delegate) + if (!IsFactoryDelegate(param2)) + { + // ONLY accept AddSingleton for instance registrations + if (symbol.Name != "AddSingleton") return null; + + // Instance registrations ONLY exist with single type parameter in .NET DI + // AddSingleton(TService implementationInstance) + // There is NO AddSingleton(TImplementation) overload + if (symbol.TypeArguments.Length != 1) return null; + + var instanceParamName = param2.Name; + + // param2.Type should be the TService type parameter from the method signature + var svcTypeParam = symbolToCheck.TypeParameters.Length >= 1 ? symbolToCheck.TypeParameters[0] : null; + if (svcTypeParam == null || !SymbolEqualityComparer.Default.Equals(param2.Type, svcTypeParam)) + return null; + + return new RegistrationValidationResult( + RegistrationKind.InstanceSingleTypeParam, + null, + null, + instanceParamName); + } + + // Factory delegate registration + if (!ValidateFactoryDelegate(param2, expectedArgCount: 2, out var funcReturnType)) return null; - var factoryParamName = factoryParam.Name; + var factoryParamName = param2.Name; if (symbol.TypeArguments.Length == 2) { @@ -141,18 +174,20 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) return new RegistrationValidationResult( RegistrationKind.FactoryTwoTypeParams, factoryParamName, + null, null); } // AddScoped(Func) // Verify factory return type matches TService (only type arg) - var svcTypeArg = symbol.TypeArguments[0]; - if (!SymbolEqualityComparer.Default.Equals(funcReturnType, svcTypeArg)) + var svcTypeArg2 = symbol.TypeArguments[0]; + if (!SymbolEqualityComparer.Default.Equals(funcReturnType, svcTypeArg2)) return null; return new RegistrationValidationResult( RegistrationKind.FactorySingleTypeParam, factoryParamName, + null, null); } @@ -179,15 +214,43 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) return new RegistrationValidationResult( RegistrationKind.KeyedParameterless, null, - serviceKeyParamName); + serviceKeyParamName, + null); } - // Keyed factory delegate registration (paramCount == 3) - var factoryParam = symbolToCheck.Parameters[2]; - if (!ValidateKeyedFactoryDelegate(factoryParam, out var funcReturnType)) + // paramCount == 3 - could be keyed factory delegate OR keyed instance registration + var param3 = symbolToCheck.Parameters[2]; + + // Check if it's a keyed instance parameter (not a delegate) + if (!IsKeyedFactoryDelegate(param3)) + { + // ONLY accept AddKeyedSingleton for keyed instance registrations + if (symbol.Name != "AddKeyedSingleton") return null; + + // Keyed instance registrations ONLY exist with single type parameter in .NET DI + // AddKeyedSingleton(object? serviceKey, TService implementationInstance) + // There is NO AddKeyedSingleton(key, TImplementation) overload + if (symbol.TypeArguments.Length != 1) return null; + + var instanceParamName = param3.Name; + + // param3.Type should be the TService type parameter from the method signature + var svcTypeParam = symbolToCheck.TypeParameters.Length >= 1 ? symbolToCheck.TypeParameters[0] : null; + if (svcTypeParam == null || !SymbolEqualityComparer.Default.Equals(param3.Type, svcTypeParam)) + return null; + + return new RegistrationValidationResult( + RegistrationKind.KeyedInstanceSingleTypeParam, + null, + serviceKeyParamName, + instanceParamName); + } + + // Keyed factory delegate registration + if (!ValidateKeyedFactoryDelegate(param3, out var funcReturnType)) return null; - var factoryParamName = factoryParam.Name; + var factoryParamName = param3.Name; if (symbol.TypeArguments.Length == 2) { @@ -195,7 +258,8 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) return new RegistrationValidationResult( RegistrationKind.KeyedFactoryTwoTypeParams, factoryParamName, - serviceKeyParamName); + serviceKeyParamName, + null); } // AddKeyedScoped(key, Func) @@ -207,7 +271,22 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) return new RegistrationValidationResult( RegistrationKind.KeyedFactorySingleTypeParam, factoryParamName, - serviceKeyParamName); + serviceKeyParamName, + null); + } + + private static bool IsKeyedFactoryDelegate(IParameterSymbol param) + { + if (param.Type is not INamedTypeSymbol namedType) return false; + var originalDef = namedType.OriginalDefinition.ToDisplayString(); + return originalDef == "System.Func"; + } + + private static bool IsFactoryDelegate(IParameterSymbol param) + { + if (param.Type is not INamedTypeSymbol namedType) return false; + var originalDef = namedType.OriginalDefinition.ToDisplayString(); + return originalDef == "System.Func" || originalDef == "System.Func"; } private static bool ValidateFactoryDelegate( @@ -256,7 +335,10 @@ private static bool ValidateKeyedFactoryDelegate( } private static (INamedTypeSymbol? Service, INamedTypeSymbol? Implementation) ExtractServiceAndImplementationTypes( - IMethodSymbol symbol) + IMethodSymbol symbol, + RegistrationKind kind, + InvocationExpressionSyntax inv, + SemanticModel semanticModel) { if (symbol.TypeArguments.Length == 2) { @@ -265,10 +347,49 @@ private static (INamedTypeSymbol? Service, INamedTypeSymbol? Implementation) Ext return (svc, impl); } - // 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); + // For single type param: AddScoped(...) + var serviceType = symbol.TypeArguments[0] as INamedTypeSymbol; + + // For instance registrations, extract the actual implementation type from the argument + if (kind == RegistrationKind.InstanceSingleTypeParam) + { + // Get the actual argument expression (the instance being passed) + // For: services.AddSingleton>(instance) + // We need to get the type of 'instance' + // Note: Extension method syntax doesn't include 'this' parameter in ArgumentList + var args = inv.ArgumentList.Arguments; + if (args.Count >= 1) // Just the instance parameter + { + var instanceArg = args[0].Expression; + var instanceType = semanticModel.GetTypeInfo(instanceArg).Type as INamedTypeSymbol; + if (instanceType != null) + { + return (serviceType, instanceType); + } + } + } + + // For keyed instance registrations, extract the actual implementation type from the argument + if (kind == RegistrationKind.KeyedInstanceSingleTypeParam) + { + // Get the actual argument expression (the instance being passed) + // For: services.AddKeyedSingleton>("key", instance) + // We need to get the type of 'instance' (second argument after the key) + // Note: Extension method syntax doesn't include 'this' parameter in ArgumentList + var args = inv.ArgumentList.Arguments; + if (args.Count >= 2) // Key parameter + instance parameter + { + var instanceArg = args[1].Expression; + var instanceType = semanticModel.GetTypeInfo(instanceArg).Type as INamedTypeSymbol; + if (instanceType != null) + { + return (serviceType, instanceType); + } + } + } + + // For factory delegates or if we can't determine the instance type, use the service type for both + return (serviceType, serviceType); } private static string? GetInterceptsData(SemanticModel semanticModel, InvocationExpressionSyntax inv) diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/047_InstanceRegistration_SingleTypeParam/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/047_InstanceRegistration_SingleTypeParam/Program.cs new file mode 100644 index 0000000..df9183c --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/047_InstanceRegistration_SingleTypeParam/Program.cs @@ -0,0 +1,12 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Instance registration with single type parameter +var instance = new SqlRepository(); +services.AddSingleton>(instance); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredService>(); +repo.Save(new Customer { Id = 1, Name = "John" }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/047_InstanceRegistration_SingleTypeParam/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/047_InstanceRegistration_SingleTypeParam/Repository.cs new file mode 100644 index 0000000..4922a00 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/047_InstanceRegistration_SingleTypeParam/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/048_InstanceRegistration_MultipleDecorators/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/048_InstanceRegistration_MultipleDecorators/Program.cs new file mode 100644 index 0000000..e7f9483 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/048_InstanceRegistration_MultipleDecorators/Program.cs @@ -0,0 +1,12 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Instance registration with multiple ordered decorators +var instance = new SqlRepository(); +services.AddSingleton>(instance); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredService>(); +repo.Save(new Customer { Id = 1, Name = "John" }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/048_InstanceRegistration_MultipleDecorators/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/048_InstanceRegistration_MultipleDecorators/Repository.cs new file mode 100644 index 0000000..4ff0a46 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/048_InstanceRegistration_MultipleDecorators/Repository.cs @@ -0,0 +1,56 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +[DecoratedBy(typeof(LoggingRepository<>), Order = 1)] +[DecoratedBy(typeof(CachingRepository<>), Order = 2)] +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 CachingRepository : IRepository +{ + private readonly IRepository _inner; + + public CachingRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Cache] Invalidating 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/049_InstanceRegistration_NoDecorators/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/049_InstanceRegistration_NoDecorators/Program.cs new file mode 100644 index 0000000..0d2c4fd --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/049_InstanceRegistration_NoDecorators/Program.cs @@ -0,0 +1,12 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Instance registration without decorators +var instance = new SqlRepository(); +services.AddSingleton>(instance); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredService>(); +repo.Save(new Customer { Id = 1, Name = "John" }); diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/049_InstanceRegistration_NoDecorators/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/049_InstanceRegistration_NoDecorators/Repository.cs new file mode 100644 index 0000000..6fc4da8 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/049_InstanceRegistration_NoDecorators/Repository.cs @@ -0,0 +1,21 @@ +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +// No decorators - should pass through without interception +public class SqlRepository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[SQL] Saving {typeof(T).Name}..."); + } +} + +public class Customer +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/051_KeyedInstanceRegistration_SingleDecorator/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/051_KeyedInstanceRegistration_SingleDecorator/Program.cs new file mode 100644 index 0000000..a2f5bcc --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/051_KeyedInstanceRegistration_SingleDecorator/Program.cs @@ -0,0 +1,12 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Keyed instance registration with single decorator +var instance = new SqlRepository(); +services.AddKeyedSingleton>("my-key", instance); + +var serviceProvider = services.BuildServiceProvider(); +var repo = serviceProvider.GetRequiredKeyedService>("my-key"); +repo.Save(new Customer { Id = 1, Name = "John" }); \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/051_KeyedInstanceRegistration_SingleDecorator/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/051_KeyedInstanceRegistration_SingleDecorator/Repository.cs new file mode 100644 index 0000000..7094780 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/051_KeyedInstanceRegistration_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; +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs index 469a8a6..5b68e28 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs @@ -575,4 +575,57 @@ await VerifyGlue.VerifySourcesAsync(sut, ], featureFlags: FeatureFlags); } + + [Theory] + [GeneratorAutoData] + public async Task InstanceRegistration_SingleTypeParam(DecoWeaverGenerator sut) + { + // Test instance registration with single type parameter + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/047_InstanceRegistration_SingleTypeParam/Repository.cs", + "Cases/047_InstanceRegistration_SingleTypeParam/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task InstanceRegistration_MultipleDecorators(DecoWeaverGenerator sut) + { + // Test instance registration with multiple ordered decorators + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/048_InstanceRegistration_MultipleDecorators/Repository.cs", + "Cases/048_InstanceRegistration_MultipleDecorators/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task InstanceRegistration_NoDecorators(DecoWeaverGenerator sut) + { + // Test instance registration without decorators - should pass through + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/049_InstanceRegistration_NoDecorators/Repository.cs", + "Cases/049_InstanceRegistration_NoDecorators/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task KeyedInstanceRegistration_SingleDecorator(DecoWeaverGenerator sut) + { + // Test keyed instance registration with single decorator + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/051_KeyedInstanceRegistration_SingleDecorator/Repository.cs", + "Cases/051_KeyedInstanceRegistration_SingleDecorator/Program.cs" + ], + featureFlags: FeatureFlags); + } + } \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.InstanceRegistration_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.InstanceRegistration_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..e8307cd --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.InstanceRegistration_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,79 @@ +//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: "++ogaUd5p3qQhTQ4TFlQJ+QAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddSingleton<global::DecoWeaver.Sample.IRepository>(IServiceCollection, global::DecoWeaver.Sample.IRepository) + internal static IServiceCollection AddSingleton_0(this IServiceCollection services, TService implementationInstance) + where TService : class + { + // Register the undecorated instance as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.SqlRepository)); + var capturedInstance = (global::DecoWeaver.Sample.IRepository)(object)implementationInstance; + services.AddKeyedSingleton>(key, capturedInstance); + + // Register factory that applies decorators around the instance + services.AddSingleton>(sp => + { + var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + // Compose decorators (innermost to outermost) + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.LoggingRepository<>), current); + 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.InstanceRegistration_SingleTypeParam_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.InstanceRegistration_SingleTypeParam_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..20ba07e --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.InstanceRegistration_SingleTypeParam_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,78 @@ +//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: "BmQyizj89hET/xx3USIwmd4AAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddSingleton<global::DecoWeaver.Sample.IRepository>(IServiceCollection, global::DecoWeaver.Sample.IRepository) + internal static IServiceCollection AddSingleton_0(this IServiceCollection services, TService implementationInstance) + where TService : class + { + // Register the undecorated instance as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.SqlRepository)); + var capturedInstance = (global::DecoWeaver.Sample.IRepository)(object)implementationInstance; + services.AddKeyedSingleton>(key, capturedInstance); + + // Register factory that applies decorators around the instance + services.AddSingleton>(sp => + { + var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + // 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.KeyedInstanceRegistration_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedInstanceRegistration_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..09b6367 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.KeyedInstanceRegistration_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: "W/IS2NoNOxnW5QMq1/XlXd8AAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddKeyedSingleton<global::DecoWeaver.Sample.IRepository>(IServiceCollection, object?, global::DecoWeaver.Sample.IRepository) + internal static IServiceCollection AddKeyedSingleton_0(this IServiceCollection services, object? serviceKey, TService implementationInstance) + where TService : class + { + // Create nested key to avoid circular resolution + var nestedKey = DecoratorKeys.ForKeyed(serviceKey, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.SqlRepository)); + var capturedInstance = (global::DecoWeaver.Sample.IRepository)(object)implementationInstance; + + // Register the undecorated instance with nested key + services.AddKeyedSingleton>(nestedKey, capturedInstance); + + // Register factory with user's key that applies decorators around the instance + services.AddKeyedSingleton>(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); + } + } + + } +}