diff --git a/CLAUDE.md b/CLAUDE.md index 06d145a..2611778 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -175,16 +175,48 @@ Tests live in `/test/Cases/{NNN}_{Description}/` directories: - Open generic decorators are closed at runtime via `MakeGenericType` when the service is resolved - Type arguments extracted from the service type being resolved -**Supported Registration Pattern**: +**Supported Registration Patterns**: ```csharp -// βœ… Closed generic registration - INTERCEPTED by DecoWeaver +// βœ… Closed generic registration (parameterless) - INTERCEPTED by DecoWeaver services.AddScoped, Repository>(); services.AddScoped, Repository>(); +// βœ… Closed generic registration with factory delegate - INTERCEPTED by DecoWeaver (v1.0.2+) +services.AddScoped, Repository>(sp => new Repository()); +services.AddScoped>(sp => new Repository()); + // ❌ Open generic registration - NOT intercepted services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); ``` +### Factory Delegate Support (v1.0.2+) + +DecoWeaver supports factory delegate registrations for all three lifetimes: + +**Two-parameter generic factory**: +```csharp +services.AddScoped, Repository>(sp => new Repository(...)); +services.AddTransient, Repository>(sp => new Repository(...)); +services.AddSingleton, Repository>(sp => new Repository(...)); +``` + +**Single-parameter generic factory**: +```csharp +services.AddScoped>(sp => new Repository(...)); +``` + +**Complex dependencies** are supported - factories can resolve dependencies from `IServiceProvider`: +```csharp +services.AddScoped, Repository>(sp => +{ + var logger = sp.GetRequiredService>>(); + var config = sp.GetRequiredService(); + return new Repository(logger, config); +}); +``` + +Decorators are applied around the factory result, and the factory logic is preserved. + ### Attribute Compilation - Attributes marked with `[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")]` diff --git a/Directory.Build.props b/Directory.Build.props index bf97eca..4001cbd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ - 1.0.1-beta + 1.0.2-beta MIT diff --git a/README.md b/README.md index b1518eb..69a38bb 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]` +- **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 - **Generic Type Decoration**: Decorate generic types like `IRepository` with open generic decorators diff --git a/docs/changelog.md b/docs/changelog.md index 1f272dc..c15b68b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - No changes yet +## [1.0.2-beta] - 2025-11-12 + +### Added +- **Factory delegate support** - Decorators now work with factory delegate registrations + - `AddScoped(sp => new Implementation(...))` - Two-parameter generic overload + - `AddScoped(sp => new Implementation(...))` - Single-parameter generic overload + - All lifetimes supported: `AddScoped`, `AddTransient`, `AddSingleton` + - Factory delegates can resolve dependencies from `IServiceProvider` + - Decorator preservation - Factory logic is preserved while decorators are applied around the result + +### Changed +- Extended `ClosedGenericRegistrationProvider` to detect and intercept factory delegate signatures +- Updated `InterceptorEmitter` to generate correct code for factory overloads +- Factory delegates are registered as keyed services, then wrapped with decorators +- Test case 022 renamed to `FactoryDelegate_SingleDecorator` to reflect new behavior + +### Technical Details +- Added `RegistrationKind` enum (Parameterless, FactoryTwoTypeParams, FactorySingleTypeParam) +- Extended `ClosedGenericRegistration` model with optional `FactoryParameterName` field +- Factory transformers detect `Func` signatures +- Generated interceptors preserve user's factory logic in keyed service registration +- 6 new test cases (033-038) covering factory delegate scenarios +- 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 ### Added diff --git a/docs/index.md b/docs/index.md index 6a33e0b..6312153 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,6 +13,7 @@ - **πŸ”§ Simple API**: Apply decorators with `[DecoratedBy]` or `[assembly: DecorateService(...)]` - **🌐 Assembly-Level Decorators**: Apply decorators to all implementations from one place - **πŸš€ Generic Type Decoration**: Decorate generic types like `IRepository` with open generic decorators +- **🏭 Factory Delegate Support**: Works with factory registrations like `AddScoped(sp => new Impl(...))` - **🚫 Opt-Out Support**: Exclude specific decorators with `[DoNotDecorate]` - **πŸ“¦ No Runtime Dependencies**: Only build-time source generator dependency - **πŸ”— Order Control**: Explicit decorator ordering via `Order` property diff --git a/docs/usage/class-level-decorators.md b/docs/usage/class-level-decorators.md index e3f5a21..3b0fa32 100644 --- a/docs/usage/class-level-decorators.md +++ b/docs/usage/class-level-decorators.md @@ -113,6 +113,112 @@ services.AddTransient(); DecoWeaver intercepts these registration calls and wraps your implementation with the decorator. +## Factory Delegate Registration + +!!! info "New in v1.0.2-beta" + Factory delegate support was added in version 1.0.2-beta. + +DecoWeaver also supports factory delegate registrations, allowing you to use custom initialization logic while still applying decorators: + +### Two-Parameter Generic Factory + +```csharp +[DecoratedBy] +public class UserRepository : IUserRepository +{ + // Your implementation +} + +// Factory delegate with two type parameters +services.AddScoped(sp => + new UserRepository()); +``` + +### Single-Parameter Generic Factory + +```csharp +[DecoratedBy] +public class UserRepository : IUserRepository +{ + // Your implementation +} + +// Factory delegate with single type parameter +services.AddScoped(sp => + new UserRepository()); +``` + +### Complex Dependencies + +Factory delegates can resolve dependencies from the `IServiceProvider`: + +```csharp +[DecoratedBy] +public class UserRepository : IUserRepository +{ + private readonly ILogger _logger; + private readonly IOptions _options; + + public UserRepository(ILogger logger, IOptions options) + { + _logger = logger; + _options = options; + } + + // Implementation +} + +// Register with factory that resolves dependencies +services.AddScoped(sp => +{ + var loggerFactory = sp.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + var options = sp.GetRequiredService>(); + + return new UserRepository(logger, options); +}); +``` + +### How It Works + +When using factory delegates: + +1. **Your factory logic is preserved** - The lambda you provide is captured and used +2. **Decorators are applied around the result** - DecoWeaver wraps the factory's output +3. **All lifetimes are supported** - `AddScoped`, `AddTransient`, `AddSingleton` + +The generated code: +- Registers your factory as a keyed service +- Creates an outer factory that calls your factory and applies decorators +- Maintains the same dependency resolution behavior you defined + +```csharp +// What you write: +services.AddScoped(sp => + new UserRepository(sp.GetRequiredService())); + +// What happens (conceptually): +// 1. Your factory is registered as keyed service +// 2. Outer factory applies decorators: +var repo = /* your factory result */; +var cached = new CachingRepository(repo); +var logged = new LoggingRepository(cached); +// 3. Logged instance is returned +``` + +### Factory Delegate Limitations + +Factory delegates work with: +- βœ… Generic registration methods: `AddScoped(factory)`, `AddScoped(factory)` +- βœ… 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 + ## Decorator Dependencies Decorators can have their own dependencies, resolved from the DI container: diff --git a/releasenotes.props b/releasenotes.props index b553bde..b48baf3 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -1,6 +1,6 @@ - + + + + + + + + + (sp => new TImpl(...))` - Two-parameter generic overload + - `AddScoped(sp => new TImpl(...))` - Single-parameter generic overload + - All lifetimes supported: `AddScoped`, `AddTransient`, `AddSingleton` +* **Complex dependency resolution** - Factory delegates can resolve dependencies from `IServiceProvider` +* **Decorator preservation** - Factory logic is preserved while decorators are applied around the result + +### What's Changed + +* Extended `ClosedGenericRegistrationProvider` to detect and intercept factory delegate signatures +* Updated `InterceptorEmitter` to generate correct code for factory overloads +* Factory delegates are registered as keyed services, then wrapped with decorators +* All existing functionality remains unchanged - this is purely additive + +### Documentation + +* Updated sample project with factory delegate examples +* Added comprehensive test coverage (6 new test cases) +* Full documentation: https://layeredcraft.github.io/decoweaver/ +* GitHub: https://github.com/layeredcraft/decoweaver + ]]> diff --git a/samples/DecoWeaver.Sample/DecoWeaver.Sample.csproj b/samples/DecoWeaver.Sample/DecoWeaver.Sample.csproj index 485758b..c8eceda 100644 --- a/samples/DecoWeaver.Sample/DecoWeaver.Sample.csproj +++ b/samples/DecoWeaver.Sample/DecoWeaver.Sample.csproj @@ -17,7 +17,9 @@ - + + + diff --git a/samples/DecoWeaver.Sample/Program.cs b/samples/DecoWeaver.Sample/Program.cs index dd7fd98..8759864 100644 --- a/samples/DecoWeaver.Sample/Program.cs +++ b/samples/DecoWeaver.Sample/Program.cs @@ -1,29 +1,54 @@ ο»Ώusing Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using DecoWeaver.Sample; // NOTE: DecoWeaver requires closed generic registrations to apply decorators. // Open generic registrations like AddScoped(typeof(IRepository<>), typeof(DynamoDbRepository<>)) // are not supported and will fall through to standard DI registration without decorators. var serviceProvider = new ServiceCollection() - // Example 1: Open generic repository with typeof() syntax + // Example 1: Open generic repository with typeof() syntax (parameterless) .AddScoped, DynamoDbRepository>() - // Example 2: Concrete service with generic attribute syntax + // Example 2: Concrete service with generic attribute syntax (parameterless) .AddScoped() + // Example 3: Factory delegate with simple logic + .AddScoped, DynamoDbRepository>(sp => + new DynamoDbRepository()) + // Example 4: Factory delegate with complex dependencies + .AddSingleton(sp => LoggerFactory.Create(builder => builder.AddConsole())) + .AddScoped, RepositoryWithLogger>(sp => + { + var loggerFactory = sp.GetRequiredService(); + var logger = loggerFactory.CreateLogger>(); + return new RepositoryWithLogger(logger); + }) .BuildServiceProvider(); -// Test Example 1: Open generic repository -Console.WriteLine("=== Example 1: Open Generic Repository [DecoratedBy(typeof(...))] ==="); +// Test Example 1: Open generic repository (parameterless) +Console.WriteLine("=== Example 1: Open Generic Repository [Parameterless] ==="); var customerRepo = serviceProvider.GetRequiredService>(); Console.WriteLine($"Resolved: {customerRepo.GetType().Name}"); customerRepo.Save(new Customer { Id = 1, Name = "John Doe" }); Console.WriteLine(); -// Test Example 2: Concrete service with generic syntax -Console.WriteLine("=== Example 2: Concrete Service [DecoratedBy] ==="); +// Test Example 2: Concrete service (parameterless) +Console.WriteLine("=== Example 2: Concrete Service [Parameterless] ==="); var userService = serviceProvider.GetRequiredService(); -userService.CreateUser("John Doe"); Console.WriteLine($"Resolved: {userService.GetType().Name}"); -userService.CreateUser("Jane Smith"); +userService.CreateUser("John Doe"); +Console.WriteLine(); + +// Test Example 3: Factory delegate with simple logic +Console.WriteLine("=== Example 3: Factory Delegate (Simple) ==="); +var orderRepo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {orderRepo.GetType().Name}"); +orderRepo.Save(new Order { Id = 1, Total = 99.99m }); +Console.WriteLine(); + +// Test Example 4: Factory delegate with complex dependencies +Console.WriteLine("=== Example 4: Factory Delegate (Complex Dependencies) ==="); +var productRepo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {productRepo.GetType().Name}"); +productRepo.Save(new Product { Id = 1, Name = "Widget" }); public class Customer { diff --git a/samples/DecoWeaver.Sample/Repository.cs b/samples/DecoWeaver.Sample/Repository.cs index 405a1e0..b03d761 100644 --- a/samples/DecoWeaver.Sample/Repository.cs +++ b/samples/DecoWeaver.Sample/Repository.cs @@ -1,4 +1,5 @@ using DecoWeaver.Attributes; +using Microsoft.Extensions.Logging; namespace DecoWeaver.Sample; @@ -151,3 +152,28 @@ public void DoSomething(T item) _inner.DoSomething(item); } } + +// ============================================================================ +// Example 3 & 4: Factory delegate examples with dependencies +// ============================================================================ +[DecoratedBy(typeof(CachingRepository<>))] +public sealed class RepositoryWithLogger : IRepository +{ + private readonly Microsoft.Extensions.Logging.ILogger _logger; + + public RepositoryWithLogger(Microsoft.Extensions.Logging.ILogger logger) + { + _logger = logger; + } + + public void Save(T item) + { + _logger.LogInformation("Saving in {RepositoryType}, type: {TypeName}", nameof(RepositoryWithLogger), typeof(T).Name); + } +} + +public class Product +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} diff --git a/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs b/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs index 475ecd4..d3ea617 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Emit/InterceptorEmitter.cs @@ -87,6 +87,34 @@ private static void EmitSingleInterceptor( // Emit the [InterceptsLocation] attribute sb.AppendLine($" [InterceptsLocation(version: 1, data: {Escape(reg.InterceptsData)})]"); + // Branch on registration kind to emit the correct signature + switch (reg.Kind) + { + case RegistrationKind.Parameterless: + EmitParameterlessInterceptor(sb, methodName, methodIndex, serviceFqn, implFqn, decorators); + break; + + case RegistrationKind.FactoryTwoTypeParams: + EmitFactoryTwoTypeParamsInterceptor(sb, methodName, reg, methodIndex, serviceFqn, implFqn, decorators); + break; + + case RegistrationKind.FactorySingleTypeParam: + EmitFactorySingleTypeParamInterceptor(sb, methodName, reg, methodIndex, serviceFqn, implFqn, decorators); + break; + + default: + throw new InvalidOperationException($"Unsupported registration kind: {reg.Kind}"); + } + } + + private static void EmitParameterlessInterceptor( + StringBuilder sb, + string methodName, + int methodIndex, + string serviceFqn, + string implFqn, + string[] decorators) + { // Emit the method - MUST be generic to match the original signature sb.AppendLine($" /// Intercepted: ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}, {implFqn}>(IServiceCollection)"); sb.AppendLine($" internal static IServiceCollection {methodName}_{methodIndex}(this IServiceCollection services)"); @@ -121,6 +149,95 @@ private static void EmitSingleInterceptor( sb.AppendLine(); } + private static void EmitFactoryTwoTypeParamsInterceptor( + StringBuilder sb, + string methodName, + ClosedGenericRegistration reg, + int methodIndex, + string serviceFqn, + string implFqn, + string[] decorators) + { + var factoryParamName = reg.FactoryParameterName ?? "implementationFactory"; + + // Emit the method signature matching AddScoped(services, Func) + sb.AppendLine($" /// Intercepted: ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}, {implFqn}>(IServiceCollection, Func<IServiceProvider, {implFqn}>)"); + sb.AppendLine($" internal static IServiceCollection {methodName}_{methodIndex}(this IServiceCollection services, Func {factoryParamName})"); + sb.AppendLine(" where TService : class"); + sb.AppendLine(" where TImplementation : class, TService"); + sb.AppendLine(" {"); + + if (decorators.Length > 0) + { + sb.AppendLine(" // Register the undecorated implementation as a keyed service with factory"); + sb.AppendLine($" var key = DecoratorKeys.For(typeof({serviceFqn}), typeof({implFqn}));"); + sb.AppendLine($" services.{AddKeyed(methodName)}<{serviceFqn}>(key, (sp, _) => ({serviceFqn}){factoryParamName}(sp));"); + sb.AppendLine(); + sb.AppendLine(" // Register factory that applies decorators"); + 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 factory"); + sb.AppendLine($" return Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}, {implFqn}>(services, {factoryParamName});"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } + + private static void EmitFactorySingleTypeParamInterceptor( + StringBuilder sb, + string methodName, + ClosedGenericRegistration reg, + int methodIndex, + string serviceFqn, + string implFqn, + string[] decorators) + { + var factoryParamName = reg.FactoryParameterName ?? "implementationFactory"; + + // Emit the method signature matching AddScoped(services, Func) + sb.AppendLine($" /// Intercepted: ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}>(IServiceCollection, Func<IServiceProvider, {serviceFqn}>)"); + sb.AppendLine($" internal static IServiceCollection {methodName}_{methodIndex}(this IServiceCollection services, Func {factoryParamName})"); + sb.AppendLine(" where TService : class"); + sb.AppendLine(" {"); + + if (decorators.Length > 0) + { + sb.AppendLine(" // Register the undecorated implementation as a keyed service with factory"); + sb.AppendLine($" var key = DecoratorKeys.For(typeof({serviceFqn}), typeof({implFqn}));"); + sb.AppendLine($" services.{AddKeyed(methodName)}<{serviceFqn}>(key, (sp, _) => {factoryParamName}(sp));"); + sb.AppendLine(); + sb.AppendLine(" // Register factory that applies decorators"); + 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 factory"); + sb.AppendLine($" return Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.{methodName}<{serviceFqn}>(services, {factoryParamName});"); + } + + sb.AppendLine(" }"); + sb.AppendLine(); + } + private static string AddKeyed(string lifetimeMethod) => lifetimeMethod switch { diff --git a/src/LayeredCraft.DecoWeaver.Generators/LayeredCraft.DecoWeaver.Generators.csproj b/src/LayeredCraft.DecoWeaver.Generators/LayeredCraft.DecoWeaver.Generators.csproj index eba7850..10a2824 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/LayeredCraft.DecoWeaver.Generators.csproj +++ b/src/LayeredCraft.DecoWeaver.Generators/LayeredCraft.DecoWeaver.Generators.csproj @@ -39,5 +39,8 @@ + + + diff --git a/src/LayeredCraft.DecoWeaver.Generators/Model/DecoratorToIntercept.cs b/src/LayeredCraft.DecoWeaver.Generators/Model/DecoratorToIntercept.cs index 7b5aa41..e7455ee 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Model/DecoratorToIntercept.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Model/DecoratorToIntercept.cs @@ -1,5 +1,3 @@ -using DecoWeaver.Util; - namespace DecoWeaver.Model; /// @@ -18,29 +16,6 @@ public enum DiLifetime : byte { Transient, Scoped, Singleton } /// Compact file location for diagnostics/dedup. public readonly record struct LocationId(string FilePath, int Start, int Length); -/// -/// Identity of a type definition (no type arguments). -/// Stable across compilations: namespace, metadata name, arity, assembly. -/// -public readonly record struct TypeDefId( - string AssemblyName, - EquatableArray ContainingNamespaces, // outermost -> innermost - EquatableArray ContainingTypes, // for nested types, outermost -> innermost (metadata names) - string MetadataName, // e.g., "IRepository`1" - int Arity // 0 for non-generic; matches `N in `N -); - -/// -/// A (possibly generic) type: definition + zero or more type arguments. -/// For open generics, TypeArgs.Length == 0 and Arity > 0 (unbound). -/// For closed generics, TypeArgs.Length == Arity and each arg is a TypeId. -/// -public readonly record struct TypeId(TypeDefId Definition, TypeId[] TypeArgs) -{ - public bool IsOpenGeneric => Definition.Arity > 0 && (TypeArgs is null || TypeArgs.Length == 0); - public bool IsClosedGeneric => Definition.Arity == TypeArgs.Length && Definition.Arity > 0; -} - /// A single ServiceCollection registration we discovered. public readonly record struct RegistrationOccurrence( DiLifetime Lifetime, diff --git a/src/LayeredCraft.DecoWeaver.Generators/Model/TypeDefId.cs b/src/LayeredCraft.DecoWeaver.Generators/Model/TypeDefId.cs new file mode 100644 index 0000000..6f7984d --- /dev/null +++ b/src/LayeredCraft.DecoWeaver.Generators/Model/TypeDefId.cs @@ -0,0 +1,57 @@ +using DecoWeaver.Util; +using Microsoft.CodeAnalysis; + +namespace DecoWeaver.Model; + +/// +/// Identity of a type definition (no type arguments). +/// Stable across compilations: namespace, metadata name, arity, assembly. +/// +public readonly record struct TypeDefId( + string AssemblyName, + EquatableArray ContainingNamespaces, // outermost -> innermost + EquatableArray ContainingTypes, // for nested types, outermost -> innermost (metadata names) + string MetadataName, // e.g., "IRepository`1" + int Arity // 0 for non-generic; matches `N in `N +); + +internal static class TypeDefIdExtensions +{ + extension(TypeDefId typeDefId) + { + internal static TypeDefId Create(ITypeSymbol t) + { + // For named types: collect containers + assembly + var assembly = t.ContainingAssembly?.Name ?? "unknown"; + + // Walk containing types (for nested) + var containingTypes = new Stack(); + var curType = t.ContainingType; + while (curType is not null) + { + containingTypes.Push(curType.MetadataName); // includes `N + curType = curType.ContainingType; + } + + // Walk namespaces + var nsParts = new Stack(); + var ns = t.ContainingNamespace; + while (ns is { IsGlobalNamespace: false }) + { + nsParts.Push(ns.Name); + ns = ns.ContainingNamespace; + } + + var metadataName = (t as INamedTypeSymbol)?.MetadataName ?? t.Name; // includes `N for generics + var arity = (t as INamedTypeSymbol)?.Arity ?? 0; + + return new( + AssemblyName: assembly, + ContainingNamespaces: new EquatableArray(nsParts.ToArray()), + ContainingTypes: new EquatableArray(containingTypes.ToArray()), + MetadataName: metadataName, + Arity: arity + ); + } + } +} \ No newline at end of file diff --git a/src/LayeredCraft.DecoWeaver.Generators/Model/TypeId.cs b/src/LayeredCraft.DecoWeaver.Generators/Model/TypeId.cs new file mode 100644 index 0000000..2958805 --- /dev/null +++ b/src/LayeredCraft.DecoWeaver.Generators/Model/TypeId.cs @@ -0,0 +1,96 @@ +using Microsoft.CodeAnalysis; + +namespace DecoWeaver.Model; + +/// +/// A (possibly generic) type: definition + zero or more type arguments. +/// For open generics, TypeArgs.Length == 0 and Arity > 0 (unbound). +/// For closed generics, TypeArgs.Length == Arity and each arg is a TypeId. +/// +public readonly record struct TypeId(TypeDefId Definition, TypeId[] TypeArgs) +{ + public bool IsOpenGeneric => Definition.Arity > 0 && (TypeArgs is null || TypeArgs.Length == 0); + public bool IsClosedGeneric => Definition.Arity == TypeArgs.Length && Definition.Arity > 0; +} + +internal static class TypeIdExtensions +{ + extension(TypeId typeId) + { + internal static TypeId Create(ITypeSymbol t) + { + switch (t) + { + // Normalize: for constructed generics keep args; for unbound keep definition. + case INamedTypeSymbol nt: + { + var def = nt.ConstructedFrom; // canonical definition (…`N) + var defId = TypeDefId.Create(def); + + // Open generic via typeof(Foo<>) or IsUnboundGenericType: + if (nt.IsUnboundGenericType) + { + return new(defId, TypeArgs: []); + } + + // Closed generic or non-generic: + if (defId.Arity == 0) + { + return new(defId, TypeArgs: []); + } + + var args = new TypeId[nt.TypeArguments.Length]; + for (var i = 0; i < args.Length; i++) + { + // For things like T in IRepository, type arguments can be type parameters. + // Represent type parameters by their definition id (no args). + args[i] = nt.TypeArguments[i] switch + { + INamedTypeSymbol named => TypeId.Create(named), + IArrayTypeSymbol arr => TypeId.CreateFromArray(arr), + ITypeParameterSymbol tp => TypeId.CreateFromParameter(tp), + _ => throw new NotSupportedException($"Unsupported type argument: {nt.TypeArguments[i]}") + }; + } + return new(defId, args); + } + case IArrayTypeSymbol at: + return TypeId.CreateFromArray(at); + case ITypeParameterSymbol tp2: + return TypeId.CreateFromParameter(tp2); + default: + // primitives, pointers, function pointers rarely appear in DI registrations; handle primitives via metadata + return new(TypeDefId.Create(t), TypeArgs: []); + } + + } + + private static TypeId CreateFromArray(IArrayTypeSymbol at) + { + // Represent T[] as a pseudo generic: System.Array`1 (or your own convention) + // Simpler: treat "T[]" as definition "SZArray" + elem type arg + var def = new TypeDefId( + AssemblyName: "mscorlib", + ContainingNamespaces: ["System","Runtime"], + ContainingTypes: [], + MetadataName: "SZArray`1", + Arity: 1); + + return new(def, TypeArgs: [TypeId.Create(at.ElementType)]); + } + + private static TypeId CreateFromParameter(ITypeParameterSymbol tp) + { + // Represent type parameters by a pseudo definition local to their owner + var ownerName = tp.ContainingSymbol?.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat) ?? "Owner"; + var def = new TypeDefId( + AssemblyName: tp.ContainingAssembly?.Name ?? "unknown", + ContainingNamespaces: ["__TypeParameters__", ownerName], + ContainingTypes: [], + MetadataName: tp.Name, // e.g., "T" + Arity: 0); + return new(def, TypeArgs: []); + } + + } +} \ No newline at end of file diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs index e0a5614..65631ef 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs @@ -1,18 +1,29 @@ using DecoWeaver.Model; -using DecoWeaver.Roslyn; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace DecoWeaver.Providers; +internal enum RegistrationKind +{ + /// Parameterless: AddScoped<T1, T2>() + Parameterless, + /// Factory with two type params: AddScoped<T1, T2>(Func<IServiceProvider, T2>) + FactoryTwoTypeParams, + /// Factory with single type param: AddScoped<T>(Func<IServiceProvider, T>) + FactorySingleTypeParam +} + internal readonly record struct ClosedGenericRegistration( TypeDefId ServiceDef, 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 InterceptsData // "file|start|length" + string InterceptsData, // "file|start|length" + RegistrationKind Kind = RegistrationKind.Parameterless, + string? FactoryParameterName = null // Parameter name from the original registration (e.g., "implementationFactory") ); /// @@ -47,19 +58,91 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) var name = symbol.Name; if (name is not ("AddTransient" or "AddScoped" or "AddSingleton")) return null; - // Must be generic method: AddScoped() - // Use the original symbol (not OriginalDefinition) to get the constructed type arguments - if (!symbol.IsGenericMethod || symbol.TypeArguments.Length != 2) 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; - // Only match the parameterless overload: AddScoped(this IServiceCollection services) - // Reject factory delegates, keyed services, or instance registrations // symbolToCheck is the unreduced (static extension) form, so it has IServiceCollection as first parameter - if (symbolToCheck.Parameters.Length != 1) return null; - - var svc = symbol.TypeArguments[0]; - var impl = symbol.TypeArguments[1]; - - if (svc is not INamedTypeSymbol svcNamed || impl is not INamedTypeSymbol implNamed) return null; + // 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; + } + else // paramCount == 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 + } + } + + // Extract service and implementation types + INamedTypeSymbol? svc, impl; + + if (symbol.TypeArguments.Length == 2) + { + svc = symbol.TypeArguments[0] as INamedTypeSymbol; + impl = symbol.TypeArguments[1] as INamedTypeSymbol; + } + else // symbol.TypeArguments.Length == 1 + { + // For single type param factory: AddScoped(factory) + // Both service and implementation are the same type + svc = symbol.TypeArguments[0] as INamedTypeSymbol; + impl = svc; + } + + if (svc is null || impl is null) return null; // Hash-based interceptable location (Roslyn experimental) #pragma warning disable RSEXPERIMENTAL002 @@ -73,12 +156,14 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) var implFqn = impl.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); return new ClosedGenericRegistration( - ServiceDef: svc.ToTypeId().Definition, - ImplDef: impl.ToTypeId().Definition, + ServiceDef: TypeId.Create(svc).Definition, + ImplDef: TypeId.Create(impl).Definition, ServiceFqn: serviceFqn, ImplFqn: implFqn, Lifetime: name, - InterceptsData: il.Data + InterceptsData: il.Data, + Kind: kind, + FactoryParameterName: factoryParamName ); } } diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByGenericProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByGenericProvider.cs index 35e929a..4b4155b 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByGenericProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByGenericProvider.cs @@ -1,5 +1,4 @@ using DecoWeaver.Model; -using DecoWeaver.Roslyn; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -53,8 +52,8 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) if (decoratorSym is null) continue; yield return new DecoratorToIntercept( - ImplementationDef: implDef.ToTypeId().Definition, - DecoratorDef: decoratorSym.ToTypeId().Definition, + ImplementationDef: TypeId.Create(implDef).Definition, + DecoratorDef: TypeId.Create(decoratorSym).Definition, Order: order, IsInterceptable: true); } diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByNonGenericProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByNonGenericProvider.cs index f67c77e..e80e85c 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByNonGenericProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByNonGenericProvider.cs @@ -1,5 +1,4 @@ using DecoWeaver.Model; -using DecoWeaver.Roslyn; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -58,8 +57,8 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) if (decoratorSym is null) continue; yield return new DecoratorToIntercept( - ImplementationDef: implDef.ToTypeId().Definition, - DecoratorDef: decoratorSym.ToTypeId().Definition, + ImplementationDef: TypeId.Create(implDef).Definition, + DecoratorDef: TypeId.Create(decoratorSym).Definition, Order: order, IsInterceptable: true); } diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/DoNotDecorateProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/DoNotDecorateProvider.cs index 9a06e1e..6888e25 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/DoNotDecorateProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/DoNotDecorateProvider.cs @@ -1,5 +1,4 @@ using DecoWeaver.Model; -using DecoWeaver.Roslyn; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -46,8 +45,8 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) continue; yield return new DoNotDecorateDirective( - ImplementationDef: implSym.ToTypeId().Definition, - DecoratorDef: decoratorSym.ToTypeId().Definition); + ImplementationDef: TypeId.Create(implSym).Definition, + DecoratorDef: TypeId.Create(decoratorSym).Definition); } } } \ No newline at end of file diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/ServiceDecoratedByProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/ServiceDecoratedByProvider.cs index 298623b..3f93220 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/ServiceDecoratedByProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/ServiceDecoratedByProvider.cs @@ -1,5 +1,4 @@ using DecoWeaver.Model; -using DecoWeaver.Roslyn; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -64,8 +63,8 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) yield return new ServiceDecoration( AssemblyName: assemblyName, - ServiceDef: serviceSym.ToTypeId().Definition, - DecoratorDef: decoratorSym.ToTypeId().Definition, + ServiceDef: TypeId.Create(serviceSym).Definition, + DecoratorDef: TypeId.Create(decoratorSym).Definition, Order: order); } } diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/SkipAssemblyDecoratorProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/SkipAssemblyDecoratorProvider.cs index e1f3252..dc761f2 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/SkipAssemblyDecoratorProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/SkipAssemblyDecoratorProvider.cs @@ -1,5 +1,4 @@ using DecoWeaver.Model; -using DecoWeaver.Roslyn; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -24,7 +23,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) return null; return new SkipAssemblyDecoratorsMarker( - ImplementationDef: implDef.ToTypeId().Definition + ImplementationDef: TypeId.Create(implDef).Definition ); } } \ No newline at end of file diff --git a/src/LayeredCraft.DecoWeaver.Generators/Roslyn/RoslynAdapters.cs b/src/LayeredCraft.DecoWeaver.Generators/Roslyn/RoslynAdapters.cs deleted file mode 100644 index 008825f..0000000 --- a/src/LayeredCraft.DecoWeaver.Generators/Roslyn/RoslynAdapters.cs +++ /dev/null @@ -1,117 +0,0 @@ -using DecoWeaver.Model; -using DecoWeaver.Util; -using Microsoft.CodeAnalysis; - -namespace DecoWeaver.Roslyn; - -internal static class RoslynAdapters -{ - public static TypeId ToTypeId(this ITypeSymbol t) - { - switch (t) - { - // Normalize: for constructed generics keep args; for unbound keep definition. - case INamedTypeSymbol nt: - { - var def = nt.ConstructedFrom; // canonical definition (…`N) - var defId = ToTypeDefId(def); - - // Open generic via typeof(Foo<>) or IsUnboundGenericType: - if (nt.IsUnboundGenericType) - { - return new(defId, TypeArgs: []); - } - - // Closed generic or non-generic: - if (defId.Arity == 0) - { - return new(defId, TypeArgs: []); - } - - var args = new TypeId[nt.TypeArguments.Length]; - for (var i = 0; i < args.Length; i++) - { - // For things like T in IRepository, type arguments can be type parameters. - // Represent type parameters by their definition id (no args). - args[i] = nt.TypeArguments[i] switch - { - INamedTypeSymbol named => named.ToTypeId(), - IArrayTypeSymbol arr => ArrayToTypeId(arr), - ITypeParameterSymbol tp => TypeParamToTypeId(tp), - _ => throw new NotSupportedException($"Unsupported type argument: {nt.TypeArguments[i]}") - }; - } - return new(defId, args); - } - case IArrayTypeSymbol at: - return ArrayToTypeId(at); - case ITypeParameterSymbol tp2: - return TypeParamToTypeId(tp2); - default: - // primitives, pointers, function pointers rarely appear in DI registrations; handle primitives via metadata - return new(ToTypeDefId(t), TypeArgs: []); - } - } - - private static TypeId ArrayToTypeId(IArrayTypeSymbol at) - { - // Represent T[] as a pseudo generic: System.Array`1 (or your own convention) - // Simpler: treat "T[]" as definition "SZArray" + elem type arg - var def = new TypeDefId( - AssemblyName: "mscorlib", - ContainingNamespaces: ["System","Runtime"], - ContainingTypes: [], - MetadataName: "SZArray`1", - Arity: 1); - - return new(def, TypeArgs: [at.ElementType.ToTypeId()]); - } - - private static TypeId TypeParamToTypeId(ITypeParameterSymbol tp) - { - // Represent type parameters by a pseudo definition local to their owner - var ownerName = tp.ContainingSymbol?.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat) ?? "Owner"; - var def = new TypeDefId( - AssemblyName: tp.ContainingAssembly?.Name ?? "unknown", - ContainingNamespaces: ["__TypeParameters__", ownerName], - ContainingTypes: [], - MetadataName: tp.Name, // e.g., "T" - Arity: 0); - return new(def, TypeArgs: []); - } - - private static TypeDefId ToTypeDefId(ITypeSymbol t) - { - // For named types: collect containers + assembly - var assembly = t.ContainingAssembly?.Name ?? "unknown"; - - // Walk containing types (for nested) - var containingTypes = new Stack(); - var curType = t.ContainingType; - while (curType is not null) - { - containingTypes.Push(curType.MetadataName); // includes `N - curType = curType.ContainingType; - } - - // Walk namespaces - var nsParts = new Stack(); - var ns = t.ContainingNamespace; - while (ns is { IsGlobalNamespace: false }) - { - nsParts.Push(ns.Name); - ns = ns.ContainingNamespace; - } - - var metadataName = (t as INamedTypeSymbol)?.MetadataName ?? t.Name; // includes `N for generics - var arity = (t as INamedTypeSymbol)?.Arity ?? 0; - - return new( - AssemblyName: assembly, - ContainingNamespaces: new EquatableArray(nsParts.ToArray()), - ContainingTypes: new EquatableArray(containingTypes.ToArray()), - MetadataName: metadataName, - Arity: arity - ); - } -} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_ShouldNotIntercept/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_SingleDecorator/Program.cs similarity index 51% rename from test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_ShouldNotIntercept/Program.cs rename to test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_SingleDecorator/Program.cs index 6bbed66..74b9e6a 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_ShouldNotIntercept/Program.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_SingleDecorator/Program.cs @@ -1,21 +1,20 @@ using Microsoft.Extensions.DependencyInjection; using DecoWeaver.Sample; -// This test verifies that factory delegate registrations are NOT intercepted -// Factory delegates have a different signature: AddScoped(Func) -// DecoWeaver should only intercept the parameterless overload: AddScoped() +// This test verifies that factory delegate registrations ARE intercepted (Phase 1) +// Factory delegates have signature: AddScoped(Func) +// DecoWeaver now supports both parameterless and factory delegate overloads var serviceProvider = new ServiceCollection() .AddScoped, DynamoDbRepository>(sp => new DynamoDbRepository()) .BuildServiceProvider(); -// Since this uses a factory delegate, NO interceptor should be generated -// The decorator should NOT be applied +// The interceptor should wrap the factory and apply decorators var customerRepo = serviceProvider.GetRequiredService>(); Console.WriteLine($"Resolved: {customerRepo.GetType().Name}"); -// Expected: DynamoDbRepository (not decorated) -// If this were intercepted incorrectly, the factory would be lost +// Expected: CachingRepository (decorated) +// The factory is preserved and the decorator is applied around the factory result public class Customer { diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_ShouldNotIntercept/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_SingleDecorator/Repository.cs similarity index 87% rename from test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_ShouldNotIntercept/Repository.cs rename to test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_SingleDecorator/Repository.cs index 144978c..5e61e1c 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_ShouldNotIntercept/Repository.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/022_FactoryDelegate_SingleDecorator/Repository.cs @@ -7,7 +7,7 @@ public interface IRepository void Save(T entity); } -// This decorator should NOT be applied because the registration uses a factory delegate +// This decorator SHOULD be applied even though the registration uses a factory delegate (Phase 1) [DecoratedBy(typeof(CachingRepository<>))] public class DynamoDbRepository : IRepository { diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/033_FactoryDelegate_SingleGenericParam/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/033_FactoryDelegate_SingleGenericParam/Program.cs new file mode 100644 index 0000000..08dfeb1 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/033_FactoryDelegate_SingleGenericParam/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using DecoWeaver.Sample; + +// Test single type parameter factory delegate: AddScoped(factory) +// Service and implementation are the same type +var serviceProvider = new ServiceCollection() + .AddScoped>(sp => new Repository()) + .BuildServiceProvider(); + +// The decorator should be applied +var repo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {repo.GetType().Name}"); + +// Expected: LoggingRepository (decorated) + +public class User +{ + 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/Cases/033_FactoryDelegate_SingleGenericParam/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/033_FactoryDelegate_SingleGenericParam/Repository.cs new file mode 100644 index 0000000..5dca357 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/033_FactoryDelegate_SingleGenericParam/Repository.cs @@ -0,0 +1,35 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +// Single type parameter factory: AddScoped(factory) where T is both service and implementation +[DecoratedBy(typeof(LoggingRepository<>))] +public class Repository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[Repository] 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($"[Logging] Before save..."); + _inner.Save(entity); + Console.WriteLine($"[Logging] After save..."); + } +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/034_FactoryDelegate_MultipleDecorators/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/034_FactoryDelegate_MultipleDecorators/Program.cs new file mode 100644 index 0000000..ba83209 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/034_FactoryDelegate_MultipleDecorators/Program.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using DecoWeaver.Sample; + +// Test multiple decorators with factory delegate +// Decorators should be applied in order: CachingRepository (Order=1), LoggingRepository (Order=2) +var serviceProvider = new ServiceCollection() + .AddScoped, DynamoDbRepository>(sp => + new DynamoDbRepository()) + .BuildServiceProvider(); + +// The decorators should be applied in the correct order +var repo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {repo.GetType().Name}"); + +// Expected: LoggingRepository wrapping CachingRepository wrapping DynamoDbRepository + +public class Product +{ + 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/Cases/034_FactoryDelegate_MultipleDecorators/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/034_FactoryDelegate_MultipleDecorators/Repository.cs new file mode 100644 index 0000000..e327485 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/034_FactoryDelegate_MultipleDecorators/Repository.cs @@ -0,0 +1,52 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +// Multiple decorators applied in order with factory delegate +[DecoratedBy(typeof(CachingRepository<>), Order = 1)] +[DecoratedBy(typeof(LoggingRepository<>), Order = 2)] +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..."); + _inner.Save(entity); + } +} + +public class LoggingRepository : IRepository +{ + private readonly IRepository _inner; + + public LoggingRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Logging] Before save..."); + _inner.Save(entity); + Console.WriteLine($"[Logging] After save..."); + } +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/035_FactoryDelegate_NoDecorators/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/035_FactoryDelegate_NoDecorators/Program.cs new file mode 100644 index 0000000..3de6fd0 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/035_FactoryDelegate_NoDecorators/Program.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using DecoWeaver.Sample; + +// Test factory delegate without decorators +// Should pass through to original method, preserving the factory +var serviceProvider = new ServiceCollection() + .AddScoped, Repository>(sp => + new Repository()) + .BuildServiceProvider(); + +// Should resolve the implementation directly without decoration +var repo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {repo.GetType().Name}"); + +// Expected: Repository (no decorators) + +public class Order +{ + public int Id { get; set; } + public decimal Total { get; set; } +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/035_FactoryDelegate_NoDecorators/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/035_FactoryDelegate_NoDecorators/Repository.cs new file mode 100644 index 0000000..8794314 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/035_FactoryDelegate_NoDecorators/Repository.cs @@ -0,0 +1,15 @@ +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +// No decorators - factory delegate should pass through to original method +public class Repository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[Repository] Saving {typeof(T).Name}..."); + } +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/036_FactoryDelegate_Transient/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/036_FactoryDelegate_Transient/Program.cs new file mode 100644 index 0000000..0f49bf9 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/036_FactoryDelegate_Transient/Program.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using DecoWeaver.Sample; + +// Test AddTransient with factory delegate +// Decorator should be applied to transient registrations +var serviceProvider = new ServiceCollection() + .AddTransient, Repository>(sp => + new Repository()) + .BuildServiceProvider(); + +// Should resolve with decorator +var repo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {repo.GetType().Name}"); + +// Expected: MetricsRepository wrapping Repository + +public class Item +{ + public int Id { get; set; } + public string Sku { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/036_FactoryDelegate_Transient/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/036_FactoryDelegate_Transient/Repository.cs new file mode 100644 index 0000000..d85b24f --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/036_FactoryDelegate_Transient/Repository.cs @@ -0,0 +1,34 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T entity); +} + +// Transient lifetime with factory delegate and decorator +[DecoratedBy(typeof(MetricsRepository<>))] +public class Repository : IRepository +{ + public void Save(T entity) + { + Console.WriteLine($"[Repository] Saving {typeof(T).Name}..."); + } +} + +public class MetricsRepository : IRepository +{ + private readonly IRepository _inner; + + public MetricsRepository(IRepository inner) + { + _inner = inner; + } + + public void Save(T entity) + { + Console.WriteLine($"[Metrics] Recording save operation..."); + _inner.Save(entity); + } +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/037_FactoryDelegate_Singleton/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/037_FactoryDelegate_Singleton/Program.cs new file mode 100644 index 0000000..81e4929 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/037_FactoryDelegate_Singleton/Program.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using DecoWeaver.Sample; + +// Test AddSingleton with factory delegate +// Decorator should be applied to singleton registrations +var serviceProvider = new ServiceCollection() + .AddSingleton, InMemoryCache>(sp => + new InMemoryCache()) + .BuildServiceProvider(); + +// Should resolve with decorator +var cache = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {cache.GetType().Name}"); + +// Expected: LoggingCache wrapping InMemoryCache \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/037_FactoryDelegate_Singleton/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/037_FactoryDelegate_Singleton/Repository.cs new file mode 100644 index 0000000..06bb6c0 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/037_FactoryDelegate_Singleton/Repository.cs @@ -0,0 +1,47 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface ICache +{ + void Set(string key, T value); + T? Get(string key); +} + +// Singleton lifetime with factory delegate and decorator +[DecoratedBy(typeof(LoggingCache<>))] +public class InMemoryCache : ICache +{ + public void Set(string key, T value) + { + Console.WriteLine($"[InMemoryCache] Setting {key}..."); + } + + public T? Get(string key) + { + Console.WriteLine($"[InMemoryCache] Getting {key}..."); + return default; + } +} + +public class LoggingCache : ICache +{ + private readonly ICache _inner; + + public LoggingCache(ICache inner) + { + _inner = inner; + } + + public void Set(string key, T value) + { + Console.WriteLine($"[LoggingCache] Logging Set operation..."); + _inner.Set(key, value); + } + + public T? Get(string key) + { + Console.WriteLine($"[LoggingCache] Logging Get operation..."); + return _inner.Get(key); + } +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/038_FactoryDelegate_ComplexDependencies/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/038_FactoryDelegate_ComplexDependencies/Program.cs new file mode 100644 index 0000000..3e308c3 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/038_FactoryDelegate_ComplexDependencies/Program.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using DecoWeaver.Sample; + +// Test factory delegate with complex dependencies +// Factory resolves ILogger from IServiceProvider and passes it to the constructor +var serviceProvider = new ServiceCollection() + .AddSingleton() + .AddScoped, Repository>(sp => + { + var logger = sp.GetRequiredService(); + return new Repository(logger); + }) + .BuildServiceProvider(); + +// Decorator should be applied, and factory dependencies should be resolved +var repo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {repo.GetType().Name}"); + +// Expected: CachingRepository wrapping Repository (with logger injected) + +public class Account +{ + public int Id { get; set; } + public string AccountNumber { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/038_FactoryDelegate_ComplexDependencies/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/038_FactoryDelegate_ComplexDependencies/Repository.cs new file mode 100644 index 0000000..75bd2bd --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/038_FactoryDelegate_ComplexDependencies/Repository.cs @@ -0,0 +1,51 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface ILogger +{ + void Log(string message); +} + +public class ConsoleLogger : ILogger +{ + public void Log(string message) => Console.WriteLine($"[ConsoleLogger] {message}"); +} + +public interface IRepository +{ + void Save(T entity); +} + +// Factory with complex dependencies resolved from IServiceProvider +[DecoratedBy(typeof(CachingRepository<>))] +public class Repository : IRepository +{ + private readonly ILogger _logger; + + public Repository(ILogger logger) + { + _logger = logger; + } + + public void Save(T entity) + { + _logger.Log($"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..."); + _inner.Save(entity); + } +} \ 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 2933ce4..ec79e03 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs @@ -265,14 +265,14 @@ await VerifyGlue.VerifySourcesAsync(sut, [Theory] [GeneratorAutoData] - public async Task FactoryDelegate_ShouldNotIntercept(DecoWeaverGenerator sut) + public async Task FactoryDelegate_SingleDecorator(DecoWeaverGenerator sut) { - // This test verifies that registrations with factory delegates are NOT intercepted - // Only the parameterless overload AddScoped() should be intercepted + // This test verifies that registrations with factory delegates ARE intercepted (Phase 1) + // Factory delegate overloads are now supported alongside parameterless overloads await VerifyGlue.VerifySourcesAsync(sut, [ - "Cases/022_FactoryDelegate_ShouldNotIntercept/Repository.cs", - "Cases/022_FactoryDelegate_ShouldNotIntercept/Program.cs" + "Cases/022_FactoryDelegate_SingleDecorator/Repository.cs", + "Cases/022_FactoryDelegate_SingleDecorator/Program.cs" ], featureFlags: FeatureFlags); } @@ -406,4 +406,82 @@ await VerifyGlue.VerifySourcesAsync(sut, ], featureFlags: FeatureFlags); } + + [Theory] + [GeneratorAutoData] + public async Task FactoryDelegate_SingleGenericParam(DecoWeaverGenerator sut) + { + // Test single type parameter factory: AddScoped(factory) + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/033_FactoryDelegate_SingleGenericParam/Repository.cs", + "Cases/033_FactoryDelegate_SingleGenericParam/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task FactoryDelegate_MultipleDecorators(DecoWeaverGenerator sut) + { + // Test multiple decorators with factory delegate + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/034_FactoryDelegate_MultipleDecorators/Repository.cs", + "Cases/034_FactoryDelegate_MultipleDecorators/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task FactoryDelegate_NoDecorators(DecoWeaverGenerator sut) + { + // Test factory delegate without decorators (pass-through) + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/035_FactoryDelegate_NoDecorators/Repository.cs", + "Cases/035_FactoryDelegate_NoDecorators/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task FactoryDelegate_Transient(DecoWeaverGenerator sut) + { + // Test AddTransient with factory delegate + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/036_FactoryDelegate_Transient/Repository.cs", + "Cases/036_FactoryDelegate_Transient/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task FactoryDelegate_Singleton(DecoWeaverGenerator sut) + { + // Test AddSingleton with factory delegate + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/037_FactoryDelegate_Singleton/Repository.cs", + "Cases/037_FactoryDelegate_Singleton/Program.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task FactoryDelegate_ComplexDependencies(DecoWeaverGenerator sut) + { + // Test factory delegate with complex dependencies from IServiceProvider + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/038_FactoryDelegate_ComplexDependencies/Repository.cs", + "Cases/038_FactoryDelegate_ComplexDependencies/Program.cs" + ], + featureFlags: FeatureFlags); + } } \ No newline at end of file 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 new file mode 100644 index 0000000..843191b --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_ComplexDependencies_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,71 @@ +ο»Ώ//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: "BUmebHxJMuMvWFskRebYwy8BAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.Repository>(IServiceCollection, Func<IServiceProvider, global::DecoWeaver.Sample.Repository>) + internal static IServiceCollection AddScoped_0(this IServiceCollection services, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service with factory + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.Repository)); + services.AddKeyedScoped>(key, (sp, _) => (global::DecoWeaver.Sample.IRepository)implementationFactory(sp)); + + // Register factory that applies decorators + services.AddScoped>(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.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); + } + } + + 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.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 new file mode 100644 index 0000000..420026d --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_MultipleDecorators_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,72 @@ +ο»Ώ//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: "A5UbS1XWAcgcQQfyREJP6BIBAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection, Func<IServiceProvider, global::DecoWeaver.Sample.DynamoDbRepository>) + internal static IServiceCollection AddScoped_0(this IServiceCollection services, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service with factory + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped>(key, (sp, _) => (global::DecoWeaver.Sample.IRepository)implementationFactory(sp)); + + // Register factory that applies decorators + services.AddScoped>(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.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); + } + } + + 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.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 new file mode 100644 index 0000000..a1cbee8 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_SingleDecorator_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,71 @@ +ο»Ώ//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: "lhI4eSbjuyXRktef6qBoBnEBAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection, Func<IServiceProvider, global::DecoWeaver.Sample.DynamoDbRepository>) + internal static IServiceCollection AddScoped_0(this IServiceCollection services, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service with factory + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped>(key, (sp, _) => (global::DecoWeaver.Sample.IRepository)implementationFactory(sp)); + + // Register factory that applies decorators + services.AddScoped>(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.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); + } + } + + 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.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 new file mode 100644 index 0000000..e5db754 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Singleton_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,71 @@ +ο»Ώ//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: "363yBJx9vJSjUB/nmDrUV+IAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddSingleton<global::DecoWeaver.Sample.ICache, global::DecoWeaver.Sample.InMemoryCache>(IServiceCollection, Func<IServiceProvider, global::DecoWeaver.Sample.InMemoryCache>) + internal static IServiceCollection AddSingleton_0(this IServiceCollection services, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service with factory + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.ICache), typeof(global::DecoWeaver.Sample.InMemoryCache)); + services.AddKeyedSingleton>(key, (sp, _) => (global::DecoWeaver.Sample.ICache)implementationFactory(sp)); + + // Register factory that applies decorators + services.AddSingleton>(sp => + { + var current = (global::DecoWeaver.Sample.ICache)sp.GetRequiredKeyedService>(key)!; + // Compose decorators (innermost to outermost) + current = (global::DecoWeaver.Sample.ICache)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.ICache), typeof(global::DecoWeaver.Sample.LoggingCache<>), 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); + } + } + + 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.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 new file mode 100644 index 0000000..e19617d --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.FactoryDelegate_Transient_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,71 @@ +ο»Ώ//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: "dVFO2eTzmZeaGUNgquBAXOIAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddTransient<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.Repository>(IServiceCollection, Func<IServiceProvider, global::DecoWeaver.Sample.Repository>) + internal static IServiceCollection AddTransient_0(this IServiceCollection services, Func implementationFactory) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service with factory + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.Repository)); + services.AddKeyedTransient>(key, (sp, _) => (global::DecoWeaver.Sample.IRepository)implementationFactory(sp)); + + // Register factory that applies decorators + services.AddTransient>(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.MetricsRepository<>), 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); + } + } + + 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); + } + } + + } +}