-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add instance registration support for singleton decorators (Phase 3 of #3) #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
92bc50a
feat: Add instance registration support for singleton decorators (Pha…
ncipollina 7a7b635
fix: Use direct instance registration to preserve disposal semantics
ncipollina fad4d1a
docs: Add keyed instance registration example to sample
ncipollina cc40ab6
feat: Add keyed instance registration support
ncipollina File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Customer>(); | ||
| services.AddSingleton<IRepository<Customer>>(instance); | ||
|
|
||
| // DecoWeaver will apply decorators around the instance | ||
| var repo = serviceProvider.GetRequiredService<IRepository<Customer>>(); | ||
| // Returns: LoggingRepository<Customer> wrapping SqlRepository<Customer> instance | ||
| ``` | ||
|
|
||
| ### Keyed Instance Registration | ||
|
|
||
| ```csharp | ||
| // Register a pre-created instance with a key | ||
| var instance = new SqlRepository<Customer>(); | ||
| services.AddKeyedSingleton<IRepository<Customer>>("primary", instance); | ||
|
|
||
| // Resolve using the same key | ||
| var repo = serviceProvider.GetRequiredKeyedService<IRepository<Customer>>("primary"); | ||
| // Returns: LoggingRepository<Customer> wrapping SqlRepository<Customer> 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<IRepository<Customer>>(instance); | ||
|
|
||
| // ❌ NOT supported - AddScoped doesn't have instance overload in .NET DI | ||
| services.AddScoped<IRepository<Customer>>(instance); // Compiler error | ||
|
|
||
| // ❌ NOT supported - AddTransient doesn't have instance overload in .NET DI | ||
| services.AddTransient<IRepository<Customer>>(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<IRepository<Customer>>(new SqlRepository<Customer>()); | ||
|
|
||
| // DecoWeaver sees: | ||
| // - Service type: IRepository<Customer> | ||
| // - Implementation type: SqlRepository<Customer> (extracted from "new SqlRepository<Customer>()") | ||
| ``` | ||
|
|
||
| 2. **Keyed Service Registration**: The instance is registered directly as a keyed service | ||
| ```csharp | ||
| // Generated code: | ||
| var key = DecoratorKeys.For(typeof(IRepository<Customer>), typeof(SqlRepository<Customer>)); | ||
| var capturedInstance = (IRepository<Customer>)(object)implementationInstance; | ||
| services.AddKeyedSingleton<IRepository<Customer>>(key, capturedInstance); | ||
| ``` | ||
|
|
||
| 3. **Decorator Application**: Decorators are applied around the keyed service | ||
| ```csharp | ||
| // Generated code: | ||
| services.AddSingleton<IRepository<Customer>>(sp => | ||
| { | ||
| var current = sp.GetRequiredKeyedService<IRepository<Customer>>(key); | ||
| current = (IRepository<Customer>)DecoratorFactory.Create( | ||
| sp, typeof(IRepository<Customer>), typeof(LoggingRepository<>), current); | ||
| return current; | ||
| }); | ||
| ``` | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Basic Instance Registration | ||
|
|
||
| ```csharp | ||
| [DecoratedBy<LoggingRepository<>>] | ||
| public class SqlRepository<T> : IRepository<T> | ||
| { | ||
| public void Save(T entity) | ||
| { | ||
| Console.WriteLine($"[SQL] Saving {typeof(T).Name}..."); | ||
| } | ||
| } | ||
|
|
||
| // Register pre-created instance | ||
| var instance = new SqlRepository<Customer>(); | ||
| services.AddSingleton<IRepository<Customer>>(instance); | ||
|
|
||
| // The same instance is reused for all resolutions, but wrapped with decorators | ||
| var repo1 = serviceProvider.GetRequiredService<IRepository<Customer>>(); | ||
| var repo2 = serviceProvider.GetRequiredService<IRepository<Customer>>(); | ||
| // repo1 and repo2 both wrap the same SqlRepository<Customer> instance | ||
| ``` | ||
|
|
||
| ### Multiple Decorators with Instance | ||
|
|
||
| ```csharp | ||
| [DecoratedBy<CachingRepository<>>(Order = 1)] | ||
| [DecoratedBy<LoggingRepository<>>(Order = 2)] | ||
| public class SqlRepository<T> : IRepository<T> { /* ... */ } | ||
|
|
||
| var instance = new SqlRepository<Product>(); | ||
| services.AddSingleton<IRepository<Product>>(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<Order>(connectionString) | ||
| { | ||
| CommandTimeout = TimeSpan.FromSeconds(30), | ||
| EnableRetries = true | ||
| }; | ||
|
|
||
| services.AddSingleton<IRepository<Order>>(instance); | ||
| // Decorators are applied, but the pre-configured instance is preserved | ||
| ``` | ||
|
|
||
| ### Keyed Instance with Multiple Configurations | ||
|
|
||
| ```csharp | ||
| [DecoratedBy<LoggingRepository<>>] | ||
| public class SqlRepository<T> : IRepository<T> { /* ... */ } | ||
|
|
||
| // Register multiple instances with different configurations | ||
| var primaryDb = new SqlRepository<Customer>("Server=primary;Database=Main"); | ||
| var secondaryDb = new SqlRepository<Customer>("Server=secondary;Database=Replica"); | ||
|
|
||
| services.AddKeyedSingleton<IRepository<Customer>>("primary", primaryDb); | ||
| services.AddKeyedSingleton<IRepository<Customer>>("secondary", secondaryDb); | ||
|
|
||
| // Each key resolves its own instance with decorators applied | ||
| var primary = serviceProvider.GetRequiredKeyedService<IRepository<Customer>>("primary"); | ||
| var secondary = serviceProvider.GetRequiredKeyedService<IRepository<Customer>>("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<Customer>, SqlRepository<Customer>) | ||
| } | ||
|
|
||
| // 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<Customer>), typeof(SqlRepository<Customer>)); | ||
| var capturedInstance = (IRepository<Customer>)(object)implementationInstance; | ||
| services.AddKeyedSingleton<IRepository<Customer>>(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<IRepository<Customer>>(sp => | ||
| { | ||
| var config = sp.GetRequiredService<IConfiguration>(); | ||
| return new SqlRepository<Customer>(config.GetConnectionString("Default")); | ||
| }); | ||
| ``` | ||
|
|
||
| ### Parameterless with Constructor Injection | ||
| ```csharp | ||
| // Let DI handle the construction | ||
| services.AddSingleton<IRepository<Customer>, SqlRepository<Customer>>(); | ||
| // 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The date shows November 13, 2025, but the current date is November 2025. Verify this is the intended release date or update to the actual date when this version was released/will be released.