Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ services.AddScoped<IRepository<User>>(sp => new Repository<User>());
services.AddKeyedScoped<IRepository<User>, Repository<User>>("sql");
services.AddKeyedScoped<IRepository<User>, Repository<User>>("sql", (sp, key) => new Repository<User>());

// ✅ Instance registration (singleton only) - INTERCEPTED by DecoWeaver (v1.0.4+)
services.AddSingleton<IRepository<User>>(new Repository<User>());

// ❌ Open generic registration - NOT intercepted
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
```
Expand Down Expand Up @@ -274,6 +277,49 @@ var sqlRepo = serviceProvider.GetRequiredKeyedService<IRepository<User>>("sql");
- Multiple keys for same service type work independently
- Each keyed registration is intercepted separately

### Instance Registration Support (v1.0.4+)

DecoWeaver supports singleton instance registrations. This allows decorators to be applied to pre-created instances.

**Instance registration**:
```csharp
// ✅ Supported - AddSingleton with instance
var instance = new SqlRepository<Customer>();
services.AddSingleton<IRepository<Customer>>(instance);

// ❌ NOT supported - AddScoped/AddTransient don't have instance overloads in .NET DI
services.AddScoped<IRepository<Customer>>(instance); // Compiler error
services.AddTransient<IRepository<Customer>>(instance); // Compiler error
```

**How it works**:
- Instance type is extracted from the actual argument expression using `SemanticModel.GetTypeInfo(instanceArg).Type`
- Instance is registered directly as a keyed service (preserves disposal semantics)
- Decorators are applied around the instance just like other registration types

**Generated code example**:
```csharp
// User code:
services.AddSingleton<IRepository<Customer>>(new SqlRepository<Customer>());

// What DecoWeaver generates:
var key = DecoratorKeys.For(typeof(IRepository<Customer>), typeof(SqlRepository<Customer>));
var capturedInstance = (IRepository<Customer>)(object)implementationInstance;
services.AddKeyedSingleton<IRepository<Customer>>(key, capturedInstance);

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;
});
```

**Limitations**:
- Only `AddSingleton` is supported (instance registrations don't exist for Scoped/Transient in .NET DI)
- The instance must be created before registration (can't use DI for instance construction)
- All resolutions return decorators wrapping the same singleton instance

### Attribute Compilation

- Attributes marked with `[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")]`
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="releasenotes.props" />

<PropertyGroup>
<VersionPrefix>1.0.3-beta</VersionPrefix>
<VersionPrefix>1.0.4-beta</VersionPrefix>
<!-- SPDX license identifier for MIT -->
<PackageLicenseExpression>MIT</PackageLicenseExpression>

Expand Down
6 changes: 3 additions & 3 deletions LayeredCraft.DecoWeaver.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,18 @@
<File Path=".github\workflows\docs.yml" />
</Folder>
<Folder Name="/samples/">
<Project Path="samples\DecoWeaver.Sample\DecoWeaver.Sample.csproj" Type="Classic C#" />
<Project Path="samples\DecoWeaver.Sample\DecoWeaver.Sample.csproj" Type="C#" />
</Folder>
<Folder Name="/Solution Items/">
<File Path="Directory.Build.props" />
<File Path="README.md" />
<File Path="releasenotes.props" />
</Folder>
<Folder Name="/src/">
<Project Path="src\LayeredCraft.DecoWeaver.Attributes\LayeredCraft.DecoWeaver.Attributes.csproj" Type="Classic C#" />
<Project Path="src\LayeredCraft.DecoWeaver.Attributes\LayeredCraft.DecoWeaver.Attributes.csproj" Type="C#" />
<Project Path="src\LayeredCraft.DecoWeaver.Generators\LayeredCraft.DecoWeaver.Generators.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="test\LayeredCraft.DecoWeaver.Generator.Tests\LayeredCraft.DecoWeaver.Generator.Tests.csproj" Type="Classic C#" />
<Project Path="test\LayeredCraft.DecoWeaver.Generator.Tests\LayeredCraft.DecoWeaver.Generator.Tests.csproj" Type="C#" />
</Folder>
</Solution>
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ For more examples including open generics, multiple decorators, and ordering, se
- **Class-Level Decorators**: Apply decorators to specific implementations with `[DecoratedBy<T>]`
- **Keyed Service Support**: Works with keyed service registrations like `AddKeyedScoped<T, Impl>(serviceKey)`
- **Factory Delegate Support**: Works with factory registrations like `AddScoped<T, Impl>(sp => new Impl(...))`
- **Instance Registration Support**: Works with singleton instances like `AddSingleton<T>(instance)`
- **Opt-Out Support**: Exclude specific decorators with `[DoNotDecorate]`
- **Multiple Decorators**: Stack multiple decorators with explicit ordering
- **Generic Type Decoration**: Decorate generic types like `IRepository<T>` with open generic decorators
Expand Down
233 changes: 233 additions & 0 deletions docs/advanced/instance-registrations.md
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
26 changes: 26 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- No changes yet

## [1.0.4-beta] - 2025-11-13
Copy link

Copilot AI Nov 14, 2025

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.

Suggested change
## [1.0.4-beta] - 2025-11-13
## [1.0.4-beta] - [Unreleased]

Copilot uses AI. Check for mistakes.

### Added
- **Instance registration support** - Decorators now work with singleton instance registrations
- `AddSingleton<TService>(instance)` - Single type parameter with instance
- Decorators are applied around the provided instance
- Only `AddSingleton` is supported (instance registrations don't exist for Scoped/Transient in .NET DI)

### Changed
- Extended `RegistrationKind` enum with `InstanceSingleTypeParam` variant
- Added `InstanceParameterName` field to `ClosedGenericRegistration` model
- Updated `ClosedGenericRegistrationProvider` to detect instance registrations (non-delegate second parameter)
- Updated `InterceptorEmitter` to generate instance interceptors with factory lambda wrapping
- Instance type is extracted from the actual argument expression (e.g., `new SqlRepository<Customer>()`)
- Instances are registered as keyed services via factory lambda (keyed services don't have instance overloads)

### Technical Details
- Instance detection: parameter type must match type parameter and NOT be a `Func<>` delegate
- Only `AddSingleton` accepted - `AddScoped`/`AddTransient` don't support instance parameters in .NET DI
- Instance wrapped in factory lambda: `services.AddKeyedSingleton<T>(key, (sp, _) => capturedInstance)`
- Type extraction uses `SemanticModel.GetTypeInfo(instanceArg).Type` to get actual implementation type
- Extension method ArgumentList doesn't include `this` parameter, so instance is at `args[0]`
- 3 new test cases (047-049) covering instance registration scenarios
- Updated sample project with instance registration example
- All existing functionality remains unchanged - this is purely additive

## [1.0.3-beta] - 2025-11-13

### Added
Expand Down
Loading
Loading