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
36 changes: 34 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IRepository<User>, Repository<User>>();
services.AddScoped<IRepository<Product>, Repository<Product>>();

// ✅ Closed generic registration with factory delegate - INTERCEPTED by DecoWeaver (v1.0.2+)
services.AddScoped<IRepository<User>, Repository<User>>(sp => new Repository<User>());
services.AddScoped<IRepository<User>>(sp => new Repository<User>());

// ❌ 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<IRepository<T>, Repository<T>>(sp => new Repository<T>(...));
services.AddTransient<IRepository<T>, Repository<T>>(sp => new Repository<T>(...));
services.AddSingleton<IRepository<T>, Repository<T>>(sp => new Repository<T>(...));
```

**Single-parameter generic factory**:
```csharp
services.AddScoped<IRepository<T>>(sp => new Repository<T>(...));
```

**Complex dependencies** are supported - factories can resolve dependencies from `IServiceProvider`:
```csharp
services.AddScoped<IRepository<User>, Repository<User>>(sp =>
{
var logger = sp.GetRequiredService<ILogger<Repository<User>>>();
var config = sp.GetRequiredService<IConfiguration>();
return new Repository<User>(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")]`
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.1-beta</VersionPrefix>
<VersionPrefix>1.0.2-beta</VersionPrefix>
<!-- SPDX license identifier for MIT -->
<PackageLicenseExpression>MIT</PackageLicenseExpression>

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>]`
- **Factory Delegate Support**: Works with factory registrations like `AddScoped<T, Impl>(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<T>` with open generic decorators
Expand Down
25 changes: 25 additions & 0 deletions docs/changelog.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to start auto-generating this stuff 😄

Original file line number Diff line number Diff line change
Expand Up @@ -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<TService, TImplementation>(sp => new Implementation(...))` - Two-parameter generic overload
- `AddScoped<TService>(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<IServiceProvider, T>` 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
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- **🔧 Simple API**: Apply decorators with `[DecoratedBy<T>]` or `[assembly: DecorateService(...)]`
- **🌐 Assembly-Level Decorators**: Apply decorators to all implementations from one place
- **🚀 Generic Type Decoration**: Decorate generic types like `IRepository<T>` with open generic decorators
- **🏭 Factory Delegate Support**: Works with factory registrations like `AddScoped<T, Impl>(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
Expand Down
106 changes: 106 additions & 0 deletions docs/usage/class-level-decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,112 @@ services.AddTransient<IUserRepository, UserRepository>();

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<CachingRepository>]
public class UserRepository : IUserRepository
{
// Your implementation
}

// Factory delegate with two type parameters
services.AddScoped<IUserRepository, UserRepository>(sp =>
new UserRepository());
```

### Single-Parameter Generic Factory

```csharp
[DecoratedBy<LoggingRepository>]
public class UserRepository : IUserRepository
{
// Your implementation
}

// Factory delegate with single type parameter
services.AddScoped<IUserRepository>(sp =>
new UserRepository());
```

### Complex Dependencies

Factory delegates can resolve dependencies from the `IServiceProvider`:

```csharp
[DecoratedBy<CachingRepository>]
public class UserRepository : IUserRepository
{
private readonly ILogger _logger;
private readonly IOptions<DatabaseOptions> _options;

public UserRepository(ILogger logger, IOptions<DatabaseOptions> options)
{
_logger = logger;
_options = options;
}

// Implementation
}

// Register with factory that resolves dependencies
services.AddScoped<IUserRepository, UserRepository>(sp =>
{
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<UserRepository>();
var options = sp.GetRequiredService<IOptions<DatabaseOptions>>();

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<IUserRepository, UserRepository>(sp =>
new UserRepository(sp.GetRequiredService<ILogger>()));

// 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<T1, T2>(factory)`, `AddScoped<T>(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:
Expand Down
76 changes: 75 additions & 1 deletion releasenotes.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<PackageReleaseNotes Condition="'$(VersionPrefix)' == '1.0.0-beta'">
<PackageReleaseNotes Condition="$(VersionPrefix.StartsWith('1.0.0-beta'))">
<![CDATA[
## DecoWeaver 1.0.0-beta.* - Initial Beta Release

Expand Down Expand Up @@ -30,6 +30,80 @@ This is the initial beta release of DecoWeaver, a .NET incremental source genera
* GitHub: https://github.com/layeredcraft/decoweaver
* Report issues: https://github.com/layeredcraft/decoweaver/issues

]]>
</PackageReleaseNotes>

<PackageReleaseNotes Condition="$(VersionPrefix.StartsWith('1.0.1-beta'))">
<![CDATA[
## DecoWeaver 1.0.1-beta - Assembly-Level Decorator Support

This release adds assembly-level decorator support, enabling you to apply decorators to all implementations from one central location (Phase 2a of #2).

### New Features

* **Assembly-level `[DecorateService(...)]` attribute** - Apply decorators to all implementations of a service interface from one place
* **`[SkipAssemblyDecoration]` attribute** - Opt out of all assembly-level decorators for a specific implementation
* **`[DoNotDecorate(typeof(TDecorator))]` attribute** - Surgically exclude specific decorators from individual implementations
* **Merge/precedence logic** - Combines class-level and assembly-level decorators with proper ordering
* **Assembly-level decorator ordering** - Control order via `Order` property
* **Open generic matching** - Assembly-level decorators and DoNotDecorate work with open generic patterns
* **4 new test cases** (029-032) covering assembly-level decorator scenarios

### What's Changed

* Decorator discovery pipeline now includes assembly-level attribute streams
* BuildDecorationMap merges and deduplicates decorators from multiple sources
* Documentation restructured to include assembly-level decorator guides

### Technical Details

* Added `DecorateServiceAttribute` for assembly-level decoration
* Added `SkipAssemblyDecorationAttribute` for opting out of all assembly decorators
* Added `DoNotDecorateAttribute` for surgical decorator exclusion
* Added `ServiceDecoratedByProvider` for assembly attribute discovery
* Added `SkipAssemblyDecoratorProvider` for skip directive discovery
* Added `DoNotDecorateProvider` for opt-out directive discovery
* Filtering logic added to BuildDecorationMap for both skip and do-not-decorate support
* Comprehensive test coverage with snapshot verification

### Documentation

* Full documentation: https://layeredcraft.github.io/decoweaver/
* Changelog: https://layeredcraft.github.io/decoweaver/changelog/
* GitHub: https://github.com/layeredcraft/decoweaver

]]>
</PackageReleaseNotes>

<PackageReleaseNotes Condition="$(VersionPrefix.StartsWith('1.0.2-beta'))">
<![CDATA[
## DecoWeaver 1.0.2-beta - Factory Delegate Support

This release adds support for factory delegate registrations, addressing issue #3 (Phase 1).

### New Features

* **Factory delegate support** - Decorators now work with factory delegate registrations:
- `AddScoped<TService, TImpl>(sp => new TImpl(...))` - Two-parameter generic overload
- `AddScoped<TService>(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

]]>
</PackageReleaseNotes>

Expand Down
4 changes: 3 additions & 1 deletion samples/DecoWeaver.Sample/DecoWeaver.Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
</ItemGroup>
<!-- Only write generated files to disk in Debug builds (nice for inspection) -->
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
Expand Down
41 changes: 33 additions & 8 deletions samples/DecoWeaver.Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -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<IRepository<Customer>, DynamoDbRepository<Customer>>()
// Example 2: Concrete service with generic attribute syntax
// Example 2: Concrete service with generic attribute syntax (parameterless)
.AddScoped<IUserService, UserService>()
// Example 3: Factory delegate with simple logic
.AddScoped<IRepository<Order>, DynamoDbRepository<Order>>(sp =>
new DynamoDbRepository<Order>())
// Example 4: Factory delegate with complex dependencies
.AddSingleton<ILoggerFactory>(sp => LoggerFactory.Create(builder => builder.AddConsole()))
.AddScoped<IRepository<Product>, RepositoryWithLogger<Product>>(sp =>
{
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<RepositoryWithLogger<Product>>();
return new RepositoryWithLogger<Product>(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<IRepository<Customer>>();
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<T>] ===");
// Test Example 2: Concrete service (parameterless)
Console.WriteLine("=== Example 2: Concrete Service [Parameterless] ===");
var userService = serviceProvider.GetRequiredService<IUserService>();
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<IRepository<Order>>();
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<IRepository<Product>>();
Console.WriteLine($"Resolved: {productRepo.GetType().Name}");
productRepo.Save(new Product { Id = 1, Name = "Widget" });

public class Customer
{
Expand Down
Loading
Loading