Skip to content

Commit 3273c36

Browse files
ncipollinaclaude
andauthored
feat: Add instance registration support for singleton decorators (Phase 3 of #3) (#13)
* feat: Add instance registration support for singleton decorators (Phase 3 of #3) This completes issue #3 by adding support for singleton instance registrations. **New Features:** - Support for `AddSingleton<TService>(instance)` registrations - Decorators are applied around pre-created instances - Instance type extraction from argument expressions - Factory lambda wrapping for keyed service compatibility **Implementation Details:** - Added `InstanceSingleTypeParam` to `RegistrationKind` enum - Added `InstanceParameterName` field to model - Updated provider to detect non-delegate instance parameters - Extended emitter to generate instance interceptors - Instance wrapped in factory: `(sp, _) => capturedInstance` - Type extraction via `SemanticModel.GetTypeInfo(instanceArg).Type` **Limitations:** - Only `AddSingleton` supported (Scoped/Transient don't have instance overloads) - Instance must be created before registration **Testing:** - 3 new test cases (047-049) with snapshot verification - Updated sample project with instance registration example - All tests passing **Documentation:** - Created docs/advanced/instance-registrations.md - Updated CLAUDE.md with supported patterns - Updated README.md with feature list - Updated changelog and release notes - Version bumped to 1.0.4-beta 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: Use direct instance registration to preserve disposal semantics ChatGPT identified that wrapping instances in factory lambdas broke .NET DI disposal semantics. The container would dispose user-provided instances when it shouldn't. **Root Cause:** Line 421 wrapped instances in factory lambda: `(sp, _) => capturedInstance` This changed disposal ownership - factory-created services are disposed by the container, but instance registrations should NOT be. **Fix:** Use direct instance overload: `AddKeyedSingleton<T>(key, instance)` This preserves expected .NET DI disposal behavior. **Changes:** - InterceptorEmitter.cs: Removed factory lambda wrapper (line 421) - docs/advanced/instance-registrations.md: Updated to reflect direct registration - CLAUDE.md: Updated generated code example - Test snapshots regenerated with correct code **Verification:** - Build succeeds - Sample project runs correctly - Disposal semantics now match .NET DI expectations Fixes disposal bug reported by ChatGPT in PR #13 feedback. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * docs: Add keyed instance registration example to sample Added Example 8 demonstrating keyed instance registration pattern. Note: This currently does NOT apply decorators because we don't yet intercept the AddKeyedSingleton<T>(key, instance) signature. This is a known limitation and potential future enhancement. The example shows correct usage and expected resolution pattern using GetRequiredKeyedService<T>(key). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add keyed instance registration support Extends instance registration support to handle keyed singleton instances. Users can now register pre-created instances with keys and have decorators applied automatically. Key changes: - Add KeyedInstanceSingleTypeParam to RegistrationKind enum - Update ClosedGenericRegistrationProvider to detect keyed instance pattern - Validates AddKeyedSingleton<T>(key, instance) signature - Extracts implementation type from instance argument (2nd parameter) - Add EmitKeyedInstanceSingleTypeParamInterceptor to InterceptorEmitter - Uses nested key strategy to avoid circular resolution - Registers instance with nested key, decorated factory with user key - Add test case 051_KeyedInstanceRegistration_SingleDecorator - Update instance registrations documentation with keyed examples Pattern supported: services.AddKeyedSingleton<IRepository<Customer>>("primary", instance); Generated code preserves disposal semantics by using direct instance registration instead of factory lambda wrapper. Fixes sample project Example 8 - now applies decorators to keyed instances. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bd22f70 commit 3273c36

22 files changed

Lines changed: 1122 additions & 30 deletions

CLAUDE.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ services.AddScoped<IRepository<User>>(sp => new Repository<User>());
189189
services.AddKeyedScoped<IRepository<User>, Repository<User>>("sql");
190190
services.AddKeyedScoped<IRepository<User>, Repository<User>>("sql", (sp, key) => new Repository<User>());
191191

192+
// ✅ Instance registration (singleton only) - INTERCEPTED by DecoWeaver (v1.0.4+)
193+
services.AddSingleton<IRepository<User>>(new Repository<User>());
194+
192195
// ❌ Open generic registration - NOT intercepted
193196
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
194197
```
@@ -274,6 +277,49 @@ var sqlRepo = serviceProvider.GetRequiredKeyedService<IRepository<User>>("sql");
274277
- Multiple keys for same service type work independently
275278
- Each keyed registration is intercepted separately
276279

280+
### Instance Registration Support (v1.0.4+)
281+
282+
DecoWeaver supports singleton instance registrations. This allows decorators to be applied to pre-created instances.
283+
284+
**Instance registration**:
285+
```csharp
286+
// ✅ Supported - AddSingleton with instance
287+
var instance = new SqlRepository<Customer>();
288+
services.AddSingleton<IRepository<Customer>>(instance);
289+
290+
// ❌ NOT supported - AddScoped/AddTransient don't have instance overloads in .NET DI
291+
services.AddScoped<IRepository<Customer>>(instance); // Compiler error
292+
services.AddTransient<IRepository<Customer>>(instance); // Compiler error
293+
```
294+
295+
**How it works**:
296+
- Instance type is extracted from the actual argument expression using `SemanticModel.GetTypeInfo(instanceArg).Type`
297+
- Instance is registered directly as a keyed service (preserves disposal semantics)
298+
- Decorators are applied around the instance just like other registration types
299+
300+
**Generated code example**:
301+
```csharp
302+
// User code:
303+
services.AddSingleton<IRepository<Customer>>(new SqlRepository<Customer>());
304+
305+
// What DecoWeaver generates:
306+
var key = DecoratorKeys.For(typeof(IRepository<Customer>), typeof(SqlRepository<Customer>));
307+
var capturedInstance = (IRepository<Customer>)(object)implementationInstance;
308+
services.AddKeyedSingleton<IRepository<Customer>>(key, capturedInstance);
309+
310+
services.AddSingleton<IRepository<Customer>>(sp =>
311+
{
312+
var current = sp.GetRequiredKeyedService<IRepository<Customer>>(key);
313+
current = (IRepository<Customer>)DecoratorFactory.Create(sp, typeof(IRepository<Customer>), typeof(LoggingRepository<>), current);
314+
return current;
315+
});
316+
```
317+
318+
**Limitations**:
319+
- Only `AddSingleton` is supported (instance registrations don't exist for Scoped/Transient in .NET DI)
320+
- The instance must be created before registration (can't use DI for instance construction)
321+
- All resolutions return decorators wrapping the same singleton instance
322+
277323
### Attribute Compilation
278324

279325
- Attributes marked with `[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")]`

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<Import Project="releasenotes.props" />
44

55
<PropertyGroup>
6-
<VersionPrefix>1.0.3-beta</VersionPrefix>
6+
<VersionPrefix>1.0.4-beta</VersionPrefix>
77
<!-- SPDX license identifier for MIT -->
88
<PackageLicenseExpression>MIT</PackageLicenseExpression>
99

LayeredCraft.DecoWeaver.slnx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,18 +56,18 @@
5656
<File Path=".github\workflows\docs.yml" />
5757
</Folder>
5858
<Folder Name="/samples/">
59-
<Project Path="samples\DecoWeaver.Sample\DecoWeaver.Sample.csproj" Type="Classic C#" />
59+
<Project Path="samples\DecoWeaver.Sample\DecoWeaver.Sample.csproj" Type="C#" />
6060
</Folder>
6161
<Folder Name="/Solution Items/">
6262
<File Path="Directory.Build.props" />
6363
<File Path="README.md" />
6464
<File Path="releasenotes.props" />
6565
</Folder>
6666
<Folder Name="/src/">
67-
<Project Path="src\LayeredCraft.DecoWeaver.Attributes\LayeredCraft.DecoWeaver.Attributes.csproj" Type="Classic C#" />
67+
<Project Path="src\LayeredCraft.DecoWeaver.Attributes\LayeredCraft.DecoWeaver.Attributes.csproj" Type="C#" />
6868
<Project Path="src\LayeredCraft.DecoWeaver.Generators\LayeredCraft.DecoWeaver.Generators.csproj" />
6969
</Folder>
7070
<Folder Name="/test/">
71-
<Project Path="test\LayeredCraft.DecoWeaver.Generator.Tests\LayeredCraft.DecoWeaver.Generator.Tests.csproj" Type="Classic C#" />
71+
<Project Path="test\LayeredCraft.DecoWeaver.Generator.Tests\LayeredCraft.DecoWeaver.Generator.Tests.csproj" Type="C#" />
7272
</Folder>
7373
</Solution>

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ For more examples including open generics, multiple decorators, and ordering, se
7575
- **Class-Level Decorators**: Apply decorators to specific implementations with `[DecoratedBy<T>]`
7676
- **Keyed Service Support**: Works with keyed service registrations like `AddKeyedScoped<T, Impl>(serviceKey)`
7777
- **Factory Delegate Support**: Works with factory registrations like `AddScoped<T, Impl>(sp => new Impl(...))`
78+
- **Instance Registration Support**: Works with singleton instances like `AddSingleton<T>(instance)`
7879
- **Opt-Out Support**: Exclude specific decorators with `[DoNotDecorate]`
7980
- **Multiple Decorators**: Stack multiple decorators with explicit ordering
8081
- **Generic Type Decoration**: Decorate generic types like `IRepository<T>` with open generic decorators
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
# Instance Registrations
2+
3+
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.
4+
5+
## Overview
6+
7+
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.
8+
9+
## Supported Patterns
10+
11+
### Single Type Parameter with Instance
12+
13+
```csharp
14+
// Register a pre-created instance
15+
var instance = new SqlRepository<Customer>();
16+
services.AddSingleton<IRepository<Customer>>(instance);
17+
18+
// DecoWeaver will apply decorators around the instance
19+
var repo = serviceProvider.GetRequiredService<IRepository<Customer>>();
20+
// Returns: LoggingRepository<Customer> wrapping SqlRepository<Customer> instance
21+
```
22+
23+
### Keyed Instance Registration
24+
25+
```csharp
26+
// Register a pre-created instance with a key
27+
var instance = new SqlRepository<Customer>();
28+
services.AddKeyedSingleton<IRepository<Customer>>("primary", instance);
29+
30+
// Resolve using the same key
31+
var repo = serviceProvider.GetRequiredKeyedService<IRepository<Customer>>("primary");
32+
// Returns: LoggingRepository<Customer> wrapping SqlRepository<Customer> instance
33+
```
34+
35+
## Limitations
36+
37+
### Singleton Only
38+
39+
Instance registrations are **only supported with `AddSingleton`**. This is a limitation of .NET's dependency injection framework itself:
40+
41+
```csharp
42+
// ✅ Supported - AddSingleton with instance
43+
services.AddSingleton<IRepository<Customer>>(instance);
44+
45+
// ❌ NOT supported - AddScoped doesn't have instance overload in .NET DI
46+
services.AddScoped<IRepository<Customer>>(instance); // Compiler error
47+
48+
// ❌ NOT supported - AddTransient doesn't have instance overload in .NET DI
49+
services.AddTransient<IRepository<Customer>>(instance); // Compiler error
50+
```
51+
52+
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.
53+
54+
## How It Works
55+
56+
When DecoWeaver encounters an instance registration:
57+
58+
1. **Instance Type Extraction**: The generator extracts the actual type of the instance from the argument expression
59+
```csharp
60+
// User code:
61+
services.AddSingleton<IRepository<Customer>>(new SqlRepository<Customer>());
62+
63+
// DecoWeaver sees:
64+
// - Service type: IRepository<Customer>
65+
// - Implementation type: SqlRepository<Customer> (extracted from "new SqlRepository<Customer>()")
66+
```
67+
68+
2. **Keyed Service Registration**: The instance is registered directly as a keyed service
69+
```csharp
70+
// Generated code:
71+
var key = DecoratorKeys.For(typeof(IRepository<Customer>), typeof(SqlRepository<Customer>));
72+
var capturedInstance = (IRepository<Customer>)(object)implementationInstance;
73+
services.AddKeyedSingleton<IRepository<Customer>>(key, capturedInstance);
74+
```
75+
76+
3. **Decorator Application**: Decorators are applied around the keyed service
77+
```csharp
78+
// Generated code:
79+
services.AddSingleton<IRepository<Customer>>(sp =>
80+
{
81+
var current = sp.GetRequiredKeyedService<IRepository<Customer>>(key);
82+
current = (IRepository<Customer>)DecoratorFactory.Create(
83+
sp, typeof(IRepository<Customer>), typeof(LoggingRepository<>), current);
84+
return current;
85+
});
86+
```
87+
88+
## Examples
89+
90+
### Basic Instance Registration
91+
92+
```csharp
93+
[DecoratedBy<LoggingRepository<>>]
94+
public class SqlRepository<T> : IRepository<T>
95+
{
96+
public void Save(T entity)
97+
{
98+
Console.WriteLine($"[SQL] Saving {typeof(T).Name}...");
99+
}
100+
}
101+
102+
// Register pre-created instance
103+
var instance = new SqlRepository<Customer>();
104+
services.AddSingleton<IRepository<Customer>>(instance);
105+
106+
// The same instance is reused for all resolutions, but wrapped with decorators
107+
var repo1 = serviceProvider.GetRequiredService<IRepository<Customer>>();
108+
var repo2 = serviceProvider.GetRequiredService<IRepository<Customer>>();
109+
// repo1 and repo2 both wrap the same SqlRepository<Customer> instance
110+
```
111+
112+
### Multiple Decorators with Instance
113+
114+
```csharp
115+
[DecoratedBy<CachingRepository<>>(Order = 1)]
116+
[DecoratedBy<LoggingRepository<>>(Order = 2)]
117+
public class SqlRepository<T> : IRepository<T> { /* ... */ }
118+
119+
var instance = new SqlRepository<Product>();
120+
services.AddSingleton<IRepository<Product>>(instance);
121+
122+
// Resolved as: LoggingRepository wrapping CachingRepository wrapping instance
123+
```
124+
125+
### Pre-Configured Instance
126+
127+
```csharp
128+
// Useful when instance needs complex initialization
129+
var connectionString = configuration.GetConnectionString("Production");
130+
var instance = new SqlRepository<Order>(connectionString)
131+
{
132+
CommandTimeout = TimeSpan.FromSeconds(30),
133+
EnableRetries = true
134+
};
135+
136+
services.AddSingleton<IRepository<Order>>(instance);
137+
// Decorators are applied, but the pre-configured instance is preserved
138+
```
139+
140+
### Keyed Instance with Multiple Configurations
141+
142+
```csharp
143+
[DecoratedBy<LoggingRepository<>>]
144+
public class SqlRepository<T> : IRepository<T> { /* ... */ }
145+
146+
// Register multiple instances with different configurations
147+
var primaryDb = new SqlRepository<Customer>("Server=primary;Database=Main");
148+
var secondaryDb = new SqlRepository<Customer>("Server=secondary;Database=Replica");
149+
150+
services.AddKeyedSingleton<IRepository<Customer>>("primary", primaryDb);
151+
services.AddKeyedSingleton<IRepository<Customer>>("secondary", secondaryDb);
152+
153+
// Each key resolves its own instance with decorators applied
154+
var primary = serviceProvider.GetRequiredKeyedService<IRepository<Customer>>("primary");
155+
var secondary = serviceProvider.GetRequiredKeyedService<IRepository<Customer>>("secondary");
156+
// Both are wrapped with LoggingRepository, but use different SqlRepository instances
157+
```
158+
159+
## Technical Details
160+
161+
### Type Extraction from Arguments
162+
163+
DecoWeaver uses Roslyn's semantic model to extract the actual type from the instance argument:
164+
165+
```csharp
166+
// In ClosedGenericRegistrationProvider.cs - Non-keyed instance
167+
var args = inv.ArgumentList.Arguments;
168+
if (args.Count >= 1)
169+
{
170+
var instanceArg = args[0].Expression; // Extension methods don't include 'this' in ArgumentList
171+
var instanceType = semanticModel.GetTypeInfo(instanceArg).Type as INamedTypeSymbol;
172+
return (serviceType, instanceType); // e.g., (IRepository<Customer>, SqlRepository<Customer>)
173+
}
174+
175+
// For keyed instances
176+
if (args.Count >= 2) // Key parameter + instance parameter
177+
{
178+
var instanceArg = args[1].Expression; // Second argument after the key
179+
var instanceType = semanticModel.GetTypeInfo(instanceArg).Type as INamedTypeSymbol;
180+
return (serviceType, instanceType);
181+
}
182+
```
183+
184+
### Direct Instance Registration
185+
186+
DecoWeaver uses the direct instance overload available in .NET DI for keyed singleton services:
187+
188+
```csharp
189+
// DecoWeaver generates:
190+
var key = DecoratorKeys.For(typeof(IRepository<Customer>), typeof(SqlRepository<Customer>));
191+
var capturedInstance = (IRepository<Customer>)(object)implementationInstance;
192+
services.AddKeyedSingleton<IRepository<Customer>>(key, capturedInstance);
193+
```
194+
195+
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.
196+
197+
The double cast `(TService)(object)` ensures the generic type parameter `TService` is compatible with the captured instance.
198+
199+
## When to Use Instance Registrations
200+
201+
Instance registrations with DecoWeaver are useful when:
202+
203+
1. **Pre-configured Dependencies**: Your instance needs complex initialization that's easier to do outside of DI
204+
2. **External Resources**: Registering wrappers around external resources (e.g., database connections, message queues)
205+
3. **Testing/Mocking**: Registering test doubles or mocks with specific configurations
206+
4. **Singleton State**: When you need a true singleton with decorators applied
207+
208+
## Alternatives
209+
210+
If you need more flexibility, consider these alternatives:
211+
212+
### Factory Delegates
213+
```csharp
214+
// More flexible than instances - can use IServiceProvider
215+
services.AddSingleton<IRepository<Customer>>(sp =>
216+
{
217+
var config = sp.GetRequiredService<IConfiguration>();
218+
return new SqlRepository<Customer>(config.GetConnectionString("Default"));
219+
});
220+
```
221+
222+
### Parameterless with Constructor Injection
223+
```csharp
224+
// Let DI handle the construction
225+
services.AddSingleton<IRepository<Customer>, SqlRepository<Customer>>();
226+
// SqlRepository constructor receives dependencies from DI
227+
```
228+
229+
## See Also
230+
231+
- [Factory Delegates](../usage/factory-delegates.md) - Using factory functions with decorators
232+
- [Keyed Services](keyed-services.md) - How DecoWeaver uses keyed services internally
233+
- [How It Works](../core-concepts/how-it-works.md) - Understanding the generation process

docs/changelog.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- No changes yet
1212

13+
## [1.0.4-beta] - 2025-11-13
14+
15+
### Added
16+
- **Instance registration support** - Decorators now work with singleton instance registrations
17+
- `AddSingleton<TService>(instance)` - Single type parameter with instance
18+
- Decorators are applied around the provided instance
19+
- Only `AddSingleton` is supported (instance registrations don't exist for Scoped/Transient in .NET DI)
20+
21+
### Changed
22+
- Extended `RegistrationKind` enum with `InstanceSingleTypeParam` variant
23+
- Added `InstanceParameterName` field to `ClosedGenericRegistration` model
24+
- Updated `ClosedGenericRegistrationProvider` to detect instance registrations (non-delegate second parameter)
25+
- Updated `InterceptorEmitter` to generate instance interceptors with factory lambda wrapping
26+
- Instance type is extracted from the actual argument expression (e.g., `new SqlRepository<Customer>()`)
27+
- Instances are registered as keyed services via factory lambda (keyed services don't have instance overloads)
28+
29+
### Technical Details
30+
- Instance detection: parameter type must match type parameter and NOT be a `Func<>` delegate
31+
- Only `AddSingleton` accepted - `AddScoped`/`AddTransient` don't support instance parameters in .NET DI
32+
- Instance wrapped in factory lambda: `services.AddKeyedSingleton<T>(key, (sp, _) => capturedInstance)`
33+
- Type extraction uses `SemanticModel.GetTypeInfo(instanceArg).Type` to get actual implementation type
34+
- Extension method ArgumentList doesn't include `this` parameter, so instance is at `args[0]`
35+
- 3 new test cases (047-049) covering instance registration scenarios
36+
- Updated sample project with instance registration example
37+
- All existing functionality remains unchanged - this is purely additive
38+
1339
## [1.0.3-beta] - 2025-11-13
1440

1541
### Added

0 commit comments

Comments
 (0)