diff --git a/Directory.Build.props b/Directory.Build.props index a892d49..bf97eca 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ - 1.0.0-beta + 1.0.1-beta MIT diff --git a/LayeredCraft.DecoWeaver.slnx b/LayeredCraft.DecoWeaver.slnx index 6448925..1f254db 100644 --- a/LayeredCraft.DecoWeaver.slnx +++ b/LayeredCraft.DecoWeaver.slnx @@ -41,6 +41,7 @@ + diff --git a/README.md b/README.md index cdd9957..b1518eb 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ DecoWeaver is a .NET incremental source generator that brings **compile-time dec - ⚡ **Zero runtime overhead** - No reflection or assembly scanning at startup - 🎯 **Type-safe** - Catches configuration errors at compile time - 🚀 **Fast** - Incremental generation with Roslyn -- 🔧 **Simple** - Just add `[DecoratedBy]` attributes +- 🔧 **Simple** - Class-level or assembly-level decorator attributes +- 🌐 **Flexible** - Apply decorators globally or per-implementation - 📦 **Clean** - Generates readable, debuggable interceptor code 📚 **[View Full Documentation](https://layeredcraft.github.io/decoweaver/)** @@ -70,6 +71,9 @@ For more examples including open generics, multiple decorators, and ordering, se ## Key Features +- **Assembly-Level Decorators**: Apply decorators to all implementations from one place with `[assembly: DecorateService(...)]` +- **Class-Level Decorators**: Apply decorators to specific implementations with `[DecoratedBy]` +- **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 - **Type-Safe**: Compile-time validation catches errors early diff --git a/docs/api-reference/attributes.md b/docs/api-reference/attributes.md index db9e6ec..9f40e35 100644 --- a/docs/api-reference/attributes.md +++ b/docs/api-reference/attributes.md @@ -284,8 +284,364 @@ public class OrderService : IOrderService { } [DecoratedBy(typeof(LoggingRepository))] ``` +## DecorateServiceAttribute + +Assembly-level attribute for applying decorators to all implementations of a service interface. + +### Syntax + +```csharp +[assembly: DecorateService(typeof(TService), typeof(TDecorator))] +[assembly: DecorateService(typeof(TService), typeof(TDecorator), Order = int)] +``` + +### Constructor + +```csharp +public DecorateServiceAttribute(Type serviceType, Type decoratorType) +``` + +### Parameters + +- `serviceType`: The service interface type (can be open generic like `IRepository<>`) +- `decoratorType`: The decorator type to apply (can be open generic like `CachingRepository<>`) + +### Properties + +#### Order + +```csharp +public int Order { get; set; } +``` + +Controls the order when multiple assembly-level decorators are applied. Lower values are applied first (closer to the implementation). + +**Default**: `0` + +### Target + +```csharp +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] +``` + +- **Target**: `AttributeTargets.Assembly` - Applied to assemblies +- **AllowMultiple**: `true` - Multiple decorators can be defined +- **Inherited**: `false` - Not inherited + +### Examples + +#### Basic Usage + +```csharp +using DecoWeaver.Attributes; + +// Apply logging to all IRepository implementations +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] +``` + +#### Multiple Decorators + +```csharp +// Apply logging, caching, and metrics to all repositories +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 1)] +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>), Order = 2)] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>), Order = 3)] +``` + +#### Non-Generic Services + +```csharp +// Apply to non-generic service +[assembly: DecorateService(typeof(IUserService), typeof(LoggingUserService))] +``` + +#### Mixed Generic/Non-Generic + +```csharp +// Open generic service, closed generic decorator +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingUserRepository))] +``` + +### Behavior + +At compile time, DecoWeaver: + +1. Discovers all `[assembly: DecorateService(...)]` attributes +2. Finds DI registrations matching the service type +3. Merges with class-level `[DecoratedBy]` attributes +4. Generates interceptor code applying all decorators in order + +### Type Matching + +Assembly-level decorators match registrations based on: + +- **Open generic** `IRepository<>` matches all closed registrations (`IRepository`, `IRepository`, etc.) +- **Closed generic** `IRepository` matches only `IRepository` +- **Non-generic** `IUserService` matches exactly + +### Combining with Class-Level + +Assembly-level and class-level decorators are merged: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 10)] + +// UserRepository.cs +[DecoratedBy>(Order = 5)] +public class UserRepository : IRepository { } +``` + +**Resulting chain**: `LoggingRepository` → `ValidationRepository` → `UserRepository` + +### Compile-Time Behavior + +This attribute is marked with `[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")]`, meaning: + +- The attribute does **not** exist in the compiled assembly +- No runtime reflection is possible +- Zero metadata footprint +- Only affects compile-time code generation + +### Requirements + +1. **Decorator must implement service interface** +2. **Decorator must have constructor accepting interface as first parameter** +3. **Only intercepts closed generic registrations** (`AddScoped, Impl>()`) +4. **Does not intercept open generic registrations** (`AddScoped(typeof(IRepo<>), typeof(Impl<>))`) + +### Best Practices + +1. **Group by concern** - Keep related decorators together +2. **Use gaps in Order** - Reserve ranges (10-19 for logging, 20-29 for caching, etc.) +3. **Document your strategy** - Comment why decorators are applied assembly-wide +4. **Centralize location** - Keep all assembly attributes in `GlobalUsings.cs` or similar + +## SkipAssemblyDecorationAttribute + +Class-level attribute for opting out of all assembly-level decorators. + +### Syntax + +```csharp +[SkipAssemblyDecoration] +``` + +### Constructor + +```csharp +public SkipAssemblyDecorationAttribute() +``` + +No parameters required. + +### Target + +```csharp +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +``` + +- **Target**: `AttributeTargets.Class` - Applied to classes +- **AllowMultiple**: `false` - Can only be applied once per class +- **Inherited**: `false` - Not inherited + +### Examples + +#### Skip All Assembly Decorators + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>))] + +// UserRepository.cs - gets all assembly decorators +public class UserRepository : IRepository { } + +// OrderRepository.cs - skips ALL assembly decorators +[SkipAssemblyDecoration] +public class OrderRepository : IRepository { } +``` + +**Result**: +- `UserRepository`: Decorated with Logging, Caching, and Metrics +- `OrderRepository`: No decorators applied + +#### Combined with Class-Level Decorators + +```csharp +// Skip assembly decorators, use class-level instead +[SkipAssemblyDecoration] +[DecoratedBy>] +public class ProductRepository : IRepository +{ + // Only ValidationRepository is applied (class-level) +} +``` + +### Behavior + +At compile time, DecoWeaver: + +1. Discovers all `[SkipAssemblyDecoration]` attributes on classes +2. Excludes ALL assembly-level decorators for that implementation +3. **Still applies** any class-level `[DecoratedBy]` decorators +4. Generates interceptor code with only class-level decorators + +### Scope + +- **Affects**: Only assembly-level `[DecorateService]` decorators +- **Does NOT affect**: Class-level `[DecoratedBy]` decorators +- **Isolation**: Only affects the specific class it's applied to + +### Use Cases + +1. **Performance-critical implementations** - Zero decorator overhead +2. **Completely different decoration strategy** - Use class-level decorators instead +3. **Clean slate needed** - Start fresh without assembly defaults +4. **Legacy code** - Gradual migration to new decoration patterns + +### Compile-Time Behavior + +This attribute is marked with `[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")]`, meaning: + +- The attribute does **not** exist in the compiled assembly +- No runtime reflection is possible +- Zero metadata footprint +- Only affects compile-time code generation + +### Comparison with DoNotDecorate + +| Attribute | Scope | Use When | +|-----------|-------|----------| +| `[SkipAssemblyDecoration]` | Removes ALL assembly decorators | Need clean slate or most decorators excluded | +| `[DoNotDecorate(typeof(...))]` | Removes specific decorator(s) | Need to exclude 1-2 decorators | + +### Best Practices + +1. **Use for clean slate** - When you want zero assembly decorators +2. **Combine with class-level** - Apply implementation-specific decorators +3. **Document why** - Add comments explaining the opt-out reason +4. **Prefer DoNotDecorate for surgical exclusions** - If only removing 1-2 decorators + +## DoNotDecorateAttribute + +Class-level attribute for excluding specific decorators from an implementation. + +### Syntax + +```csharp +[DoNotDecorate(typeof(TDecorator))] +``` + +### Constructor + +```csharp +public DoNotDecorateAttribute(Type decoratorType) +``` + +### Parameters + +- `decoratorType`: The decorator type to exclude (can be open generic like `CachingRepository<>`) + +### Target + +```csharp +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +``` + +- **Target**: `AttributeTargets.Class` - Applied to classes +- **AllowMultiple**: `true` - Multiple decorators can be excluded +- **Inherited**: `false` - Not inherited + +### Examples + +#### Opt Out of Assembly-Level Decorator + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] + +// UserRepository.cs - gets caching +public class UserRepository : IRepository { } + +// OrderRepository.cs - opts out of caching +[DoNotDecorate(typeof(CachingRepository<>))] +public class OrderRepository : IRepository { } +``` + +#### Multiple Opt-Outs + +```csharp +// Opt out of caching and metrics +[DoNotDecorate(typeof(CachingRepository<>))] +[DoNotDecorate(typeof(MetricsRepository<>))] +public class OrderRepository : IRepository { } +``` + +#### Open Generic Matching + +```csharp +// Open generic in DoNotDecorate matches all closed generics +[DoNotDecorate(typeof(CachingRepository<>))] // Matches CachingRepository +public class UserRepository : IRepository { } +``` + +### Behavior + +At compile time, DecoWeaver: + +1. Discovers all `[DoNotDecorate]` attributes on classes +2. Collects decorators from class-level and assembly-level sources +3. **Filters out** decorators matching the `DoNotDecorate` directives +4. Generates interceptor code with only remaining decorators + +### Type Matching Rules + +- **Open generic** `typeof(Decorator<>)` matches all closed variants +- **Closed generic** `typeof(Decorator)` matches only that specific type +- **Non-generic** types match exactly +- Type matching is by **definition** (ignoring type arguments) + +### Isolation + +`[DoNotDecorate]` only affects the specific class it's applied to: + +```csharp +[DoNotDecorate(typeof(CachingRepository<>))] +public class OrderRepository : IRepository { } + +// UserRepository still gets caching (not affected) +public class UserRepository : IRepository { } +``` + +### Use Cases + +1. **Excluding assembly-level decorators** - Primary use case +2. **Performance-critical code** - Remove observability overhead +3. **Implementation-specific constraints** - Special requirements +4. **Testing implementations** - Simplify test setup + +### Compile-Time Behavior + +This attribute is marked with `[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")]`, meaning: + +- The attribute does **not** exist in the compiled assembly +- No runtime reflection is possible +- Zero metadata footprint +- Only affects compile-time code generation + +### Best Practices + +1. **Use sparingly** - If many implementations opt out, reconsider assembly-level decorator +2. **Document why** - Comment explaining the opt-out reason +3. **Exact type match** - Ensure decorator type exactly matches what's being applied +4. **Prefer class-level for specific needs** - Use assembly-level for cross-cutting concerns + ## See Also - [Class-Level Decorators](../usage/class-level-decorators.md) - Usage guide +- [Assembly-Level Decorators](../usage/assembly-level-decorators.md) - Assembly-wide decoration +- [Opt-Out](../usage/opt-out.md) - Excluding decorators - [Multiple Decorators](../usage/multiple-decorators.md) - Stacking decorators - [Order and Nesting](../core-concepts/order-and-nesting.md) - Understanding order \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 68f61dd..1f272dc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - No changes yet +## [1.0.1-beta] - 2025-01-XX + +### Added +- Assembly-level `[DecorateService(typeof(TService), typeof(TDecorator))]` attribute for applying decorators to all implementations of a service interface +- `[SkipAssemblyDecoration]` attribute for opting out of all assembly-level decorators +- `[DoNotDecorate(typeof(TDecorator))]` attribute for surgically excluding specific decorators from individual implementations +- Merge/precedence logic for combining class-level and assembly-level decorators +- Support for ordering assembly-level decorators via `Order` property +- Open generic matching in assembly-level decorators and DoNotDecorate directives +- 4 new test cases (029-032) covering assembly-level decorator scenarios + +### Changed +- Decorator discovery pipeline now includes assembly-level attribute streams +- BuildDecorationMap now 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 + ## [1.0.0-beta] ### Added @@ -103,7 +129,6 @@ Features marked for deprecation: Planned features for future releases: ### Under Consideration -- Assembly-level `[DecorateService]` attribute (See [Issue #2](https://github.com/layeredcraft/decoweaver/issues/2)) - Decorator composition helpers - Performance profiling decorators - Additional diagnostic analyzers diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 8172179..b4dd5d1 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -119,10 +119,28 @@ To see the generated interceptor code: **Rider**: Solution Explorer → Generated Files node +## Alternative: Assembly-Level Decorators + +Instead of applying decorators to each class individually, you can apply them to all implementations from one place: + +```csharp +// In GlobalUsings.cs or any .cs file +using DecoWeaver.Attributes; + +[assembly: DecorateService(typeof(IUserService), typeof(LoggingUserService))] + +// Now ALL IUserService implementations automatically get logging +public class UserService : IUserService { } +public class AdminUserService : IUserService { } +``` + +This is ideal for cross-cutting concerns like logging, metrics, or caching that apply to many services. See [Assembly-Level Decorators](../usage/assembly-level-decorators.md) for details. + ## Next Steps Now that you have a basic decorator working, explore: +- **[Assembly-Level Decorators](../usage/assembly-level-decorators.md)** - Apply decorators globally - **[Multiple Decorators](../usage/multiple-decorators.md)** - Stack decorators with ordering - **[Open Generics](../usage/open-generics.md)** - Decorate `IRepository` patterns - **[Examples](../examples/index.md)** - Real-world decorator patterns diff --git a/docs/index.md b/docs/index.md index 36f4104..6a33e0b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,8 +10,10 @@ - **⚡ Zero Runtime Overhead**: Decorators applied at compile time using C# interceptors - **🎯 Type-Safe**: Full compile-time validation with IntelliSense support -- **🔧 Simple API**: Just add `[DecoratedBy]` attributes to your classes +- **🔧 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 +- **🚫 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 - **✨ Clean Generated Code**: Readable, debuggable interceptor code diff --git a/docs/usage/assembly-level-decorators.md b/docs/usage/assembly-level-decorators.md new file mode 100644 index 0000000..74976d4 --- /dev/null +++ b/docs/usage/assembly-level-decorators.md @@ -0,0 +1,459 @@ +# Assembly-Level Decorators + +Assembly-level decorators provide a centralized way to apply decorators to multiple implementations across your codebase. Instead of applying `[DecoratedBy]` to each class individually, you can declare decorators once at the assembly level. + +## Basic Syntax + +Use the `[assembly: DecorateService(...)]` attribute in any `.cs` file (commonly in `GlobalUsings.cs` or `AssemblyInfo.cs`): + +```csharp +using DecoWeaver.Attributes; + +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] +``` + +This applies `CachingRepository<>` to **all** implementations of `IRepository<>` registered in the DI container. + +## When to Use Assembly-Level Decorators + +Assembly-level decorators are ideal for: + +### Cross-Cutting Concerns + +Apply the same decorator to many implementations: + +```csharp +// In GlobalUsings.cs or AssemblyInfo.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>))] +``` + +Now **every** `IRepository` implementation automatically gets logging and metrics. + +### Centralized Configuration + +Manage all decorators in one place instead of scattered across many classes: + +```csharp +// ❌ Before: Scattered across many files +[DecoratedBy] +public class UserRepository : IRepository { } + +[DecoratedBy] +public class ProductRepository : IRepository { } + +[DecoratedBy] +public class OrderRepository : IRepository { } + +// ✅ After: Centralized in one place +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] + +public class UserRepository : IRepository { } +public class ProductRepository : IRepository { } +public class OrderRepository : IRepository { } +``` + +### Consistency Enforcement + +Ensure all implementations follow the same patterns: + +```csharp +// Enforce observability for all repositories +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 1)] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>), Order = 2)] + +// Enforce caching for all query services +[assembly: DecorateService(typeof(IQueryService<>), typeof(CachingQueryService<>))] +``` + +## Syntax Variants + +### Open Generic Service and Decorator + +Most common - both service and decorator are generic: + +```csharp +[assembly: DecorateService( + typeof(IRepository<>), // Service type + typeof(CachingRepository<>) // Decorator type +)] +``` + +### Open Generic Service, Closed Generic Decorator + +Service is generic, decorator is closed: + +```csharp +[assembly: DecorateService( + typeof(IRepository<>), // Service type + typeof(CachingUserRepository) // Closed decorator for User only +)] +``` + +This only decorates implementations where T matches the decorator's closed type. + +### Non-Generic Service and Decorator + +Both service and decorator are concrete: + +```csharp +[assembly: DecorateService( + typeof(IUserService), + typeof(LoggingUserService) +)] +``` + +## Decorator Requirements + +Assembly-level decorators have the same requirements as class-level decorators: + +1. **Implement the service interface** +2. **Accept the interface as constructor parameter** (typically first) +3. **Have resolvable dependencies** from DI container + +```csharp +public interface IRepository +{ + Task GetByIdAsync(int id); + Task SaveAsync(T entity); +} + +// ✅ Valid assembly-level decorator +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + private readonly IMemoryCache _cache; + + public CachingRepository(IRepository inner, IMemoryCache cache) + { + _inner = inner; + _cache = cache; + } + + public async Task GetByIdAsync(int id) + { + var key = $"{typeof(T).Name}:{id}"; + if (_cache.TryGetValue(key, out T cached)) + return cached; + + var entity = await _inner.GetByIdAsync(id); + _cache.Set(key, entity, TimeSpan.FromMinutes(5)); + return entity; + } + + public Task SaveAsync(T entity) => _inner.SaveAsync(entity); +} +``` + +## Multiple Assembly-Level Decorators + +Stack multiple decorators using the `Order` property: + +```csharp +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 1)] +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>), Order = 2)] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>), Order = 3)] +``` + +**Resulting chain** for any `IRepository`: +``` +MetricsRepository + → CachingRepository + → LoggingRepository + → [Your Implementation] +``` + +Lower `Order` values are closer to the implementation (innermost). + +## Combining with Class-Level Decorators + +You can combine assembly-level and class-level decorators on the same implementation: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 10)] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>), Order = 20)] + +// UserRepository.cs +[DecoratedBy>(Order = 5)] +public class UserRepository : IRepository +{ + // Implementation +} +``` + +**Resulting chain**: +``` +MetricsRepository // Order 20 (assembly-level) + → LoggingRepository // Order 10 (assembly-level) + → ValidationRepository // Order 5 (class-level) + → UserRepository +``` + +### Precedence Rules + +When combining decorators: + +1. **All decorators are merged** (both class-level and assembly-level) +2. **Sorted by Order** property (ascending) +3. **Duplicates are removed** (same decorator type + order) +4. **Class-level takes precedence** over assembly-level for same decorator + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 10)] + +// UserRepository.cs +[DecoratedBy>(Order = 10)] // Same type and order +public class UserRepository : IRepository { } +``` + +**Result**: Only **one** `LoggingRepository` is applied (class-level takes precedence). + +## Opting Out + +DecoWeaver provides two ways to opt out of assembly-level decorators: + +### Skip All Assembly Decorators + +Use `[SkipAssemblyDecoration]` to completely bypass all assembly-level decorators: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>))] + +// UserRepository.cs - gets all three decorators +public class UserRepository : IRepository { } + +// OrderRepository.cs - skips ALL assembly decorators +[SkipAssemblyDecoration] +public class OrderRepository : IRepository { } + +// ProductRepository.cs - skips assembly, uses class-level instead +[SkipAssemblyDecoration] +[DecoratedBy>] +public class ProductRepository : IRepository { } +``` + +**Result**: +- `UserRepository`: Caching → Logging → Metrics (all assembly-level) +- `OrderRepository`: No decorators +- `ProductRepository`: Only Validation (class-level only) + +**When to use**: Performance-critical code, completely different decoration strategy, or clean slate needed. + +### Exclude Specific Decorators + +Use `[DoNotDecorate]` to surgically remove specific decorators while keeping others: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] + +// UserRepository.cs - gets both decorators +public class UserRepository : IRepository { } + +// OrderRepository.cs - opts out of caching only +[DoNotDecorate(typeof(CachingRepository<>))] +public class OrderRepository : IRepository { } + +// ProductRepository.cs - opts out of both (use SkipAssemblyDecoration instead) +[DoNotDecorate(typeof(CachingRepository<>))] +[DoNotDecorate(typeof(LoggingRepository<>))] +public class ProductRepository : IRepository { } +``` + +**When to use**: Opt out of 1-2 specific decorators while keeping the rest. + +### Choosing Between Them + +| Attribute | Scope | Use When | +|-----------|-------|----------| +| `[SkipAssemblyDecoration]` | Removes ALL assembly decorators | Need clean slate or completely different strategy | +| `[DoNotDecorate(typeof(...))]` | Removes specific decorator(s) | Need to exclude 1-2 decorators | + +!!! tip "Best Practice" + If you need to exclude most/all decorators, use `[SkipAssemblyDecoration]`. If you need to exclude just a few, use `[DoNotDecorate]`. + +See [Opt-Out](opt-out.md) for complete details and more examples. + +## Registration + +Assembly-level decorators work with any service lifetime: + +```csharp +var services = new ServiceCollection(); + +// All of these get decorated by assembly-level decorators +services.AddScoped, UserRepository>(); +services.AddSingleton, ProductRepository>(); +services.AddTransient, OrderRepository>(); +``` + +DecoWeaver automatically intercepts these registrations and applies the decorators. + +## How It Works + +At compile time, DecoWeaver: + +1. **Discovers** all `[assembly: DecorateService(...)]` attributes +2. **Finds** DI registration calls like `AddScoped, Impl>()` +3. **Matches** implementations against service types +4. **Merges** with any class-level decorators +5. **Generates** interceptor code that wraps the implementation + +No runtime reflection or assembly scanning - everything happens at build time. + +## Common Patterns + +### Observability for All Services + +```csharp +// Apply logging and metrics to all repositories +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 1)] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>), Order = 2)] + +// Apply to all query services +[assembly: DecorateService(typeof(IQueryService<>), typeof(LoggingQueryService<>), Order = 1)] +[assembly: DecorateService(typeof(IQueryService<>), typeof(MetricsQueryService<>), Order = 2)] +``` + +### Caching Layer + +```csharp +// Cache all read operations +[assembly: DecorateService(typeof(IReadRepository<>), typeof(CachingRepository<>))] + +// But not write operations (no attribute for IWriteRepository<>) +``` + +### Security Layer + +```csharp +// Enforce authorization on all commands +[assembly: DecorateService(typeof(ICommandHandler<>), typeof(AuthorizationHandler<>), Order = 1)] + +// Validate all commands +[assembly: DecorateService(typeof(ICommandHandler<>), typeof(ValidationHandler<>), Order = 2)] +``` + +### Resilience + +```csharp +// Add retry to all external service calls +[assembly: DecorateService(typeof(IExternalService), typeof(RetryDecorator<>), Order = 1)] + +// Add circuit breaker +[assembly: DecorateService(typeof(IExternalService), typeof(CircuitBreakerDecorator<>), Order = 2)] +``` + +## Organization + +### Single File Approach + +Keep all assembly-level decorators in one file: + +```csharp +// GlobalUsings.cs or AssemblyDecorators.cs +using DecoWeaver.Attributes; + +// Repositories +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 10)] +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>), Order = 20)] + +// Query Services +[assembly: DecorateService(typeof(IQueryService<>), typeof(LoggingQueryService<>), Order = 10)] +[assembly: DecorateService(typeof(IQueryService<>), typeof(CachingQueryService<>), Order = 20)] + +// Command Handlers +[assembly: DecorateService(typeof(ICommandHandler<>), typeof(ValidationHandler<>), Order = 10)] +[assembly: DecorateService(typeof(ICommandHandler<>), typeof(AuthorizationHandler<>), Order = 20)] +``` + +### Multiple File Approach + +Group by concern: + +```csharp +// Observability.Assembly.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>))] + +// Performance.Assembly.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] + +// Security.Assembly.cs +[assembly: DecorateService(typeof(ICommandHandler<>), typeof(AuthorizationHandler<>))] +``` + +## Comparison with Class-Level + +| Aspect | Assembly-Level | Class-Level | +|--------|---------------|-------------| +| **Scope** | All implementations | Single implementation | +| **Location** | Global file | On class | +| **Use Case** | Cross-cutting concerns | Specific needs | +| **Maintenance** | Centralized | Distributed | +| **Visibility** | Less obvious | More explicit | +| **Flexibility** | Can opt-out | Full control | + +**When to use each**: + +- **Assembly-level**: Cross-cutting concerns (logging, metrics, caching) +- **Class-level**: Implementation-specific decorators (validation, transformation) +- **Both**: Combine for layered concerns + +## Troubleshooting + +### Decorator Not Applied + +If your assembly-level decorator isn't being applied: + +1. **Verify attribute syntax**: Ensure `[assembly: ...]` at the start +2. **Check service type match**: Service type must match registration +3. **Rebuild**: Assembly-level changes require full rebuild +4. **Check for opt-out**: Verify no `[DoNotDecorate]` on the class +5. **Verify dependencies**: Ensure decorator dependencies are registered + +### Wrong Type Argument + +```csharp +// ❌ Error: Type argument mismatch +[assembly: DecorateService(typeof(IRepository), typeof(CachingRepository<>))] + +// ✅ Fixed: Match generic arity +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] +``` + +### Not Intercepting + +Assembly-level decorators only intercept **closed generic registrations**: + +```csharp +// ✅ Intercepted +services.AddScoped, UserRepository>(); + +// ❌ NOT intercepted (open generic registration) +services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); +``` + +DecoWeaver only intercepts the `AddScoped()` syntax. + +## Best Practices + +1. **Keep assembly-level for cross-cutting concerns** - Don't overuse +2. **Document your assembly decorators** - They're less visible than class-level +3. **Use consistent ordering strategy** - Reserve ranges for each concern (10-19 for logging, 20-29 for caching, etc.) +4. **Prefer class-level for implementation-specific logic** - More explicit and maintainable +5. **Group attributes by concern** - Makes it easier to find and modify +6. **Use DoNotDecorate sparingly** - If many implementations opt out, reconsider assembly-level + +## Next Steps + +- Learn about [Opt-Out](opt-out.md) with `[DoNotDecorate]` +- Understand [Order and Nesting](../core-concepts/order-and-nesting.md) in depth +- See how to combine with [Class-Level Decorators](class-level-decorators.md) +- Explore [Examples](../examples/index.md) of real-world usage diff --git a/docs/usage/class-level-decorators.md b/docs/usage/class-level-decorators.md index c11bf4e..e3f5a21 100644 --- a/docs/usage/class-level-decorators.md +++ b/docs/usage/class-level-decorators.md @@ -2,6 +2,9 @@ The `[DecoratedBy]` attribute is the primary way to apply decorators in DecoWeaver. Apply it to your service implementation classes to automatically wrap them with decorators. +!!! info "Assembly-Level Alternative" + For cross-cutting concerns applied to many implementations, consider using [Assembly-Level Decorators](assembly-level-decorators.md) instead. Class-level decorators are best for implementation-specific needs. + ## Basic Syntax ### Generic Attribute @@ -381,8 +384,46 @@ public class LoggingDecorator : IUserRepository 4. **Document decorator behavior** with XML comments 5. **Test decorators in isolation** with mocked inner implementations +## Comparison with Assembly-Level + +| Aspect | Class-Level | Assembly-Level | +|--------|-------------|----------------| +| **Scope** | Single implementation | All implementations | +| **Attribute Location** | On class | In global file | +| **Use Case** | Implementation-specific | Cross-cutting concerns | +| **Visibility** | More explicit | Less obvious | +| **Flexibility** | Full control | Can opt-out with `[DoNotDecorate]` | + +### When to Use Each + +**Use Class-Level When**: +- Decorator is specific to one implementation +- You want explicit, visible decoration +- Different implementations need different decorators +- Testing or development-only decorators + +**Use Assembly-Level When**: +- Same decorator applies to many implementations +- Enforcing cross-cutting concerns (logging, metrics, caching) +- Centralizing decorator configuration +- Ensuring consistency across implementations + +**Combine Both When**: +- Assembly-level for common concerns +- Class-level for implementation-specific needs + +```csharp +// GlobalUsings.cs - Common logging for all +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), Order = 10)] + +// UserRepository.cs - Add validation specific to users +[DecoratedBy>(Order = 5)] +public class UserRepository : IRepository { } +``` + ## Next Steps +- Learn about [Assembly-Level Decorators](assembly-level-decorators.md) for cross-cutting concerns - Learn about [Multiple Decorators](multiple-decorators.md) - Explore [Open Generic Support](open-generics.md) - See [Examples](../examples/index.md) of real-world usage \ No newline at end of file diff --git a/docs/usage/opt-out.md b/docs/usage/opt-out.md index dba7315..099091a 100644 --- a/docs/usage/opt-out.md +++ b/docs/usage/opt-out.md @@ -1,25 +1,438 @@ # Opt-Out Mechanisms -!!! info "Coming Soon" - This documentation is under development. Check back soon for information about opting out of decorator application. +Sometimes you need to exclude decorators from certain implementations. DecoWeaver provides two opt-out mechanisms: -## Overview +- **`[SkipAssemblyDecoration]`** - Opts out of ALL assembly-level decorators +- **`[DoNotDecorate(typeof(...))]`** - Surgically removes specific decorators -This page will cover: +## SkipAssemblyDecoration Attribute -- How to exclude specific decorators from being applied -- Global opt-out mechanisms -- Surgical opt-out for specific scenarios -- Use cases for opting out of decoration +Use `[SkipAssemblyDecoration]` to opt out of **all** assembly-level decorators while keeping class-level decorators: -## Planned Content +```csharp +// GlobalUsings.cs - Apply multiple decorators to all repositories +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>))] -- [ ] Opt-out attribute usage -- [ ] Per-decorator exclusions -- [ ] Assembly-level opt-out mechanisms (Phase 2) -- [ ] Best practices for when to use opt-outs +// UserRepository.cs - Gets all three assembly-level decorators +public class UserRepository : IRepository { } + +// OrderRepository.cs - Completely opts out of assembly decorators +[SkipAssemblyDecoration] +public class OrderRepository : IRepository { } + +// ProductRepository.cs - Opts out of assembly, adds class-level +[SkipAssemblyDecoration] +[DecoratedBy>] +public class ProductRepository : IRepository { } +``` + +**Result**: +- `UserRepository`: Logging → Caching → Metrics (all assembly-level) +- `OrderRepository`: No decorators at all +- `ProductRepository`: Only ValidationRepository (class-level only) + +### When to Use SkipAssemblyDecoration + +**Use this when:** +- The implementation needs to completely bypass all assembly-level decorators +- You want a "clean slate" to apply only specific class-level decorators +- The implementation has unique requirements incompatible with standard decorators +- Performance-critical code that should have zero decorator overhead + +**Example - Performance Critical**: +```csharp +[assembly: DecorateService(typeof(IService<>), typeof(LoggingService<>))] +[assembly: DecorateService(typeof(IService<>), typeof(MetricsService<>))] + +// High-throughput service - skip all observability +[SkipAssemblyDecoration] +public class HighThroughputService : IService { } +``` + +## DoNotDecorate Attribute + +Sometimes you need to exclude decorators from certain implementations. DecoWeaver provides the `[DoNotDecorate]` attribute for fine-grained control over decorator application. + +## Basic Syntax + +Use `[DoNotDecorate(typeof(...))]` on an implementation class to exclude a specific decorator: + +```csharp +[DoNotDecorate(typeof(CachingDecorator))] +public class UserRepository : IUserRepository +{ + // This implementation won't be cached +} +``` + +## When to Opt Out + +### Excluding Assembly-Level Decorators + +The primary use case is opting out of assembly-level decorators: + +```csharp +// GlobalUsings.cs - Apply caching to all repositories +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] + +// UserRepository.cs - Gets caching +public class UserRepository : IRepository { } + +// OrderRepository.cs - Opts out of caching +[DoNotDecorate(typeof(CachingRepository<>))] +public class OrderRepository : IRepository { } +``` + +**Result**: +- `UserRepository`: Decorated with `CachingRepository` +- `OrderRepository`: No decoration applied + +### Implementation-Specific Requirements + +Some implementations have unique constraints: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(TransactionRepository<>))] + +// Most repositories need transactions +public class UserRepository : IRepository { } + +// But this one manages its own transactions +[DoNotDecorate(typeof(TransactionRepository<>))] +public class LegacyRepository : IRepository +{ + // Custom transaction management +} +``` + +### Performance-Critical Code + +Opt out of observability decorators for hot paths: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IService<>), typeof(LoggingService<>))] +[assembly: DecorateService(typeof(IService<>), typeof(MetricsService<>))] + +// Normal service - gets logging and metrics +public class UserService : IService { } + +// Performance-critical - minimal overhead +[DoNotDecorate(typeof(LoggingService<>))] +[DoNotDecorate(typeof(MetricsService<>))] +public class HighThroughputService : IService { } +``` + +### Testing Implementations + +Opt out of decorators in test implementations: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] + +// Production implementation - gets caching +public class ProductionRepository : IRepository { } + +// Test implementation - no caching needed +[DoNotDecorate(typeof(CachingRepository<>))] +public class InMemoryRepository : IRepository { } +``` + +## Multiple Opt-Outs + +Apply multiple `[DoNotDecorate]` attributes to exclude multiple decorators: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>))] + +// Opt out of caching and metrics, keep logging +[DoNotDecorate(typeof(CachingRepository<>))] +[DoNotDecorate(typeof(MetricsRepository<>))] +public class OrderRepository : IRepository +{ + // Gets LoggingRepository only +} +``` + +## Open Generic Matching + +`[DoNotDecorate]` works with open generic types and matches all closed variants: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] + +// Opt out using open generic type +[DoNotDecorate(typeof(CachingRepository<>))] +public class UserRepository : IRepository { } +``` + +Even though the assembly decorator will try to apply `CachingRepository` (closed generic), the opt-out with `CachingRepository<>` (open generic) matches and excludes it. + +**Type Matching Rules**: +- Open generic `[DoNotDecorate(typeof(Decorator<>))]` matches all closed generics +- Closed generic `[DoNotDecorate(typeof(Decorator))]` matches only that specific closed type +- Non-generic types match exactly + +## Combining with Class-Level Decorators + +`[DoNotDecorate]` can also remove class-level decorators, though this is less common: + +```csharp +// Base class with decorator +public abstract class BaseRepository : IRepository +{ + // ... +} + +// Derived class opts out (though you could just not inherit the attribute) +[DoNotDecorate(typeof(CachingRepository<>))] +public class UserRepository : BaseRepository { } +``` + +!!! note "Attribute Inheritance" + `[DecoratedBy]` attributes are **not inherited**, so this scenario is rare. You'd typically only use `[DoNotDecorate]` to remove assembly-level decorators. + +## Isolation Behavior + +`[DoNotDecorate]` only affects the **specific implementation** it's applied to: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] + +// OrderRepository opts out +[DoNotDecorate(typeof(CachingRepository<>))] +public class OrderRepository : IRepository { } + +// UserRepository still gets caching (not affected) +public class UserRepository : IRepository { } +``` + +Each implementation is evaluated independently. + +## How It Works + +At compile time, DecoWeaver: + +1. **Discovers** all `[DoNotDecorate]` attributes +2. **Collects** decorators from both class-level and assembly-level +3. **Filters out** decorators matching the `DoNotDecorate` directives +4. **Generates** interceptor code with only the remaining decorators + +This happens at build time, so there's no runtime overhead. + +## Common Patterns + +### Selective Caching + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] + +// Read-heavy repositories get caching +public class ProductRepository : IRepository { } +public class CategoryRepository : IRepository { } + +// Write-heavy repositories opt out +[DoNotDecorate(typeof(CachingRepository<>))] +public class OrderRepository : IRepository { } + +[DoNotDecorate(typeof(CachingRepository<>))] +public class InventoryRepository : IRepository { } +``` + +### Environment-Specific Decorators + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IService<>), typeof(DetailedLoggingService<>))] + +// Most services get detailed logging in dev +public class UserService : IService { } + +// But this one is too noisy +[DoNotDecorate(typeof(DetailedLoggingService<>))] +public class ChatService : IService { } +``` + +### Gradual Migration + +When migrating to assembly-level decorators: + +```csharp +// GlobalUsings.cs - New assembly-level decorator +[assembly: DecorateService(typeof(IRepository<>), typeof(NewCachingRepository<>))] + +// New implementations use the new decorator +public class UserRepository : IRepository { } + +// Legacy implementations opt out (still using old approach) +[DoNotDecorate(typeof(NewCachingRepository<>))] +public class LegacyRepository : IRepository +{ + // Still using old caching mechanism +} +``` + +### Override Assembly Decisions + +Opt out and apply a different decorator: + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(RedisCachingRepository<>))] + +// Most repositories use Redis caching +public class UserRepository : IRepository { } + +// This one uses in-memory caching instead +[DoNotDecorate(typeof(RedisCachingRepository<>))] +[DecoratedBy>] +public class ProductRepository : IRepository { } +``` + +## Troubleshooting + +### Opt-Out Not Working + +If `[DoNotDecorate]` isn't removing the decorator: + +1. **Verify exact type match**: Type must exactly match the decorator being applied +2. **Check generic arity**: `CachingRepository<>` vs `CachingRepository` +3. **Rebuild**: Opt-out changes require full rebuild +4. **Check assembly-level decorators**: Review what's actually being applied + +### Type Name Confusion + +```csharp +// ❌ Wrong: String name doesn't work +[DoNotDecorate("CachingRepository")] + +// ✅ Correct: Use typeof +[DoNotDecorate(typeof(CachingRepository<>))] +``` + +### Namespace Mismatch + +```csharp +// GlobalUsings.cs +[assembly: DecorateService(typeof(IRepository<>), typeof(Company.Infrastructure.CachingRepository<>))] + +// ❌ Wrong: Namespace mismatch +[DoNotDecorate(typeof(CachingRepository<>))] + +// ✅ Correct: Full namespace +[DoNotDecorate(typeof(Company.Infrastructure.CachingRepository<>))] +``` + +Use fully qualified type names when necessary. + +## Best Practices + +1. **Use sparingly** - If many implementations opt out, reconsider the assembly-level decorator +2. **Document why** - Add comments explaining the opt-out reason +3. **Prefer assembly-level for most cases** - Only opt out when truly necessary +4. **Consider alternatives** - Sometimes a different interface or implementation pattern is better +5. **Group opt-outs** - If several implementations opt out of the same decorator, consider a marker interface + +## Anti-Patterns + +### Over-Using Opt-Out + +```csharp +// ❌ Bad: Most implementations opt out +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] + +[DoNotDecorate(typeof(CachingRepository<>))] +public class Repo1 : IRepository { } + +[DoNotDecorate(typeof(CachingRepository<>))] +public class Repo2 : IRepository { } + +// Only one gets caching +public class Repo3 : IRepository { } +``` + +**Better approach**: Remove assembly-level decorator, use class-level where needed. + +### Opt-Out as Default + +```csharp +// ❌ Bad: Using opt-out to avoid applying decorators +[DoNotDecorate(typeof(Decorator1))] +[DoNotDecorate(typeof(Decorator2))] +[DoNotDecorate(typeof(Decorator3))] +public class CleanRepository : IRepository { } +``` + +**Better approach**: Don't apply assembly-level decorators if most implementations don't need them. + +## Choosing Between Opt-Out Mechanisms + +| Attribute | Scope | Use Case | +|-----------|-------|----------| +| **`[SkipAssemblyDecoration]`** | Removes ALL assembly decorators | Clean slate, performance critical, completely different decoration strategy | +| **`[DoNotDecorate(typeof(...))]`** | Removes specific decorator(s) | Opt out of 1-2 decorators while keeping others | + +### Decision Tree + +``` +Need to opt out? +├─ Remove ALL assembly decorators? +│ └─ Use [SkipAssemblyDecoration] +│ +└─ Remove specific decorator(s)? + ├─ Remove 1-3 decorators? → Use [DoNotDecorate] + ├─ Remove most decorators? → Use [SkipAssemblyDecoration] + class-level + └─ Complex mix? → Reconsider assembly-level approach +``` + +### Examples + +**Scenario 1: Completely Different Decoration** +```csharp +// Most services get standard observability +[assembly: DecorateService(typeof(IService<>), typeof(LoggingService<>))] +[assembly: DecorateService(typeof(IService<>), typeof(MetricsService<>))] + +// This service has custom observability +[SkipAssemblyDecoration] +[DecoratedBy] +public class SpecialService : IService { } +``` + +**Scenario 2: Opt Out of One Decorator** +```csharp +[assembly: DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] +[assembly: DecorateService(typeof(IRepository<>), typeof(MetricsRepository<>))] + +// Keep logging and metrics, skip caching +[DoNotDecorate(typeof(CachingRepository<>))] +public class OrderRepository : IRepository { } +``` + +## Comparison: Class-Level vs Assembly-Level + +| Approach | When to Use | +|----------|-------------| +| **No assembly decorator** | Decorator applies to few implementations | +| **Assembly decorator** | Decorator applies to most implementations | +| **Assembly + SkipAssemblyDecoration** | Most use assembly, few need clean slate | +| **Assembly + DoNotDecorate** | Most use assembly, some need surgical exclusions | +| **Class-level only** | Implementation-specific decorators | ## See Also -- [Class-Level Decorators](class-level-decorators.md) - Learn about applying decorators -- [Multiple Decorators](multiple-decorators.md) - Managing multiple decorators +- [Assembly-Level Decorators](assembly-level-decorators.md) - Understanding assembly-level decoration +- [Class-Level Decorators](class-level-decorators.md) - Applying decorators to individual classes +- [Multiple Decorators](multiple-decorators.md) - Managing decorator chains +- [API Reference](../api-reference/attributes.md) - Complete attribute documentation diff --git a/mkdocs.yml b/mkdocs.yml index 0bbfa2e..2a6d766 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -111,6 +111,7 @@ nav: - Order & Nesting: core-concepts/order-and-nesting.md - Usage: - Class-Level Decorators: usage/class-level-decorators.md + - Assembly-Level Decorators: usage/assembly-level-decorators.md - Open Generics: usage/open-generics.md - Multiple Decorators: usage/multiple-decorators.md - Opt-Out: usage/opt-out.md diff --git a/samples/DecoWeaver.Sample/Globals.cs b/samples/DecoWeaver.Sample/Globals.cs new file mode 100644 index 0000000..61d3215 --- /dev/null +++ b/samples/DecoWeaver.Sample/Globals.cs @@ -0,0 +1,3 @@ +using DecoWeaver.Sample; + +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] diff --git a/samples/DecoWeaver.Sample/Repository.cs b/samples/DecoWeaver.Sample/Repository.cs index fbb7f07..405a1e0 100644 --- a/samples/DecoWeaver.Sample/Repository.cs +++ b/samples/DecoWeaver.Sample/Repository.cs @@ -105,4 +105,49 @@ public void CreateUser(string name) Console.WriteLine($"[LOG] CreateUser called with name: {name}"); _inner.CreateUser(name); } -} \ No newline at end of file +} + +public interface IAssemblyInterface +{ + void DoSomething(T item); +} + +public sealed class ConcreteClass : IAssemblyInterface +{ + public void DoSomething(T item) + { + Console.WriteLine($"Doing something with {item}"); + } +} + +public sealed class CachingDecorator : IAssemblyInterface +{ + private readonly IAssemblyInterface _inner; + + public CachingDecorator(IAssemblyInterface inner) + { + _inner = inner; + } + + public void DoSomething(T item) + { + Console.WriteLine("Caching before doing something."); + _inner.DoSomething(item); + } +} + +public sealed class LoggingDecorator : IAssemblyInterface +{ + private readonly IAssemblyInterface _inner; + + public LoggingDecorator(IAssemblyInterface inner) + { + _inner = inner; + } + + public void DoSomething(T item) + { + Console.WriteLine("Logging before doing something."); + _inner.DoSomething(item); + } +} diff --git a/src/LayeredCraft.DecoWeaver.Attributes/DecoratedByAttribute.cs b/src/LayeredCraft.DecoWeaver.Attributes/DecoratedByAttribute.cs index c8e1028..828fbf0 100644 --- a/src/LayeredCraft.DecoWeaver.Attributes/DecoratedByAttribute.cs +++ b/src/LayeredCraft.DecoWeaver.Attributes/DecoratedByAttribute.cs @@ -21,6 +21,12 @@ public sealed class DecoratedByAttribute : Attribute { /// Wrapping order; lower numbers are applied first (closest to the implementation). public int Order { get; set; } + + /// + /// Determines if the decorator should be intercepted by the source generator. + /// When false, the decorator is present but not automatically applied via code generation. + /// Used for decorators applied manually in Program.cs or for documentation purposes. + /// public bool IsInterceptable { get; set; } = true; } @@ -42,6 +48,61 @@ public sealed class DecoratedByAttribute(Type decoratorType, int order = 0) : At /// Wrapping order; lower numbers are applied first. public int Order { get; set; } = order; + + /// + /// Determines if the decorator should be intercepted by the source generator. + /// When false, the decorator is present but not automatically applied via code generation. + /// Used for decorators applied manually in Program.cs or for documentation purposes. + /// public bool IsInterceptable { get; set; } = true; } +/// +/// Declares a decorator to be applied to all registrations of the specified service type +/// within the containing assembly. Supports open generic definitions (e.g., IRepository<>). +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] +[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")] +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class DecorateServiceAttribute(Type serviceType, Type decoratorType, int order = 0) : Attribute +{ + /// The service type to decorate; must be an interface or base class. + public Type ServiceType { get; set; } = serviceType; + + /// The decorator type to apply; must implement the same service contract. + public Type DecoratorType { get; set; } = decoratorType; + + /// Wrapping order; lower numbers are applied first. + public int Order { get; set; } = order; +} + +/// +/// Opts out an implementation class from all assembly-level decorators in the same assembly. +/// Use this attribute when a specific implementation should not receive any assembly-level decorations, +/// but can still have class-level decorations applied. +/// +/// +/// This attribute only affects assembly-level decorators. +/// Class-level decorators declared directly on the implementation are still applied. +/// +[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class SkipAssemblyDecorationAttribute() : Attribute; + +/// +/// Removes a specific decorator (by type definition) from the merged decoration set. +/// Use this attribute to surgically remove individual assembly-level decorators without +/// opting out of all assembly-level decorations. +/// +/// +/// Matching is performed by type definition, ignoring type arguments. For example, +/// [DoNotDecorate(typeof(CachingRepository<>))] will remove all closed variants +/// of CachingRepository from the decoration chain. +/// +[Conditional("DECOWEAVER_EMIT_ATTRIBUTE_METADATA")] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public sealed class DoNotDecorateAttribute(Type decoratorType) : Attribute +{ + /// Decorator type definition to remove (generic definition allowed). + public Type DecoratorType { get; set; } = decoratorType; +} \ No newline at end of file diff --git a/src/LayeredCraft.DecoWeaver.Generators/AttributeNames.cs b/src/LayeredCraft.DecoWeaver.Generators/AttributeNames.cs index 587e8d9..c7f8e4b 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/AttributeNames.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/AttributeNames.cs @@ -5,8 +5,14 @@ internal static class AttributeNames // Full names with namespace (for ToDisplayString() comparisons) public const string DecoratedByAttribute = $"DecoWeaver.Attributes.{DecoratedByMetadataName}"; public const string GenericDecoratedByAttribute = $"DecoWeaver.Attributes.{GenericDecoratedByMetadataName}"; + public const string ServiceDecoratedByAttribute = $"DecoWeaver.Attributes.{ServiceDecoratedByMetadataName}"; + public const string SkipAssemblyDecorationAttribute = $"DecoWeaver.Attributes.{SkipAssemblyDecorationMetadataName}"; + public const string DoNotDecorateAttribute = $"DecoWeaver.Attributes.{DoNotDecorateMetadataName}"; // Metadata names only (for pattern matching) public const string DecoratedByMetadataName = "DecoratedByAttribute"; public const string GenericDecoratedByMetadataName = "DecoratedByAttribute`1"; + public const string ServiceDecoratedByMetadataName = "DecorateServiceAttribute"; + public const string SkipAssemblyDecorationMetadataName = "SkipAssemblyDecorationAttribute"; + public const string DoNotDecorateMetadataName = "DoNotDecorateAttribute"; } \ No newline at end of file diff --git a/src/LayeredCraft.DecoWeaver.Generators/DecoWeaverGenerator.cs b/src/LayeredCraft.DecoWeaver.Generators/DecoWeaverGenerator.cs index 8575816..1f6f81a 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/DecoWeaverGenerator.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/DecoWeaverGenerator.cs @@ -16,7 +16,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // --- Language version gate ----------------------------------------- var csharpSufficient = context.CompilationProvider .Select(static (compilation, _) => - compilation is CSharpCompilation { LanguageVersion: LanguageVersion.Default or >= LanguageVersion.CSharp11 }) + compilation is CSharpCompilation + { + LanguageVersion: LanguageVersion.Default or >= LanguageVersion.CSharp11 + }) .WithTrackingName(TrackingNames.Settings_LanguageVersionGate); context.RegisterSourceOutput( @@ -34,7 +37,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) AttributeNames.GenericDecoratedByAttribute, predicate: DecoratedByGenericProvider.Predicate, transform: DecoratedByGenericProvider.TransformMultiple) - .SelectMany(static (decorators, _) => decorators.ToImmutableArray()) // Flatten IEnumerable + .SelectMany(static (decorators, _) => + decorators.ToImmutableArray()) // Flatten IEnumerable .WithTrackingName(TrackingNames.Attr_Generic_Transform) .Where(static x => x is not null) .WithTrackingName(TrackingNames.Attr_Generic_FilterNotNull) @@ -48,13 +52,63 @@ public void Initialize(IncrementalGeneratorInitializationContext context) AttributeNames.DecoratedByAttribute, predicate: DecoratedByNonGenericProvider.Predicate, transform: DecoratedByNonGenericProvider.TransformMultiple) - .SelectMany(static (decorators, _) => decorators.ToImmutableArray()) // Flatten IEnumerable + .SelectMany(static (decorators, _) => + decorators.ToImmutableArray()) // Flatten IEnumerable .WithTrackingName(TrackingNames.Attr_NonGeneric_Transform) .Where(static x => x is not null) .WithTrackingName(TrackingNames.Attr_NonGeneric_FilterNotNull) .Select(static (x, _) => x!.Value) .WithTrackingName(TrackingNames.Attr_NonGeneric_Stream); + // --- [assembly: DecorateService(...)] stream ---------------------- + // Discovers assembly-level decorator declarations that apply to all implementations + // of a service type within the same assembly. This provides default decoration rules + // that can be overridden or opted-out of at the class level. + var serviceDecorations = context.SyntaxProvider + .ForAttributeWithMetadataName( + AttributeNames.ServiceDecoratedByAttribute, + predicate: ServiceDecoratedByProvider.Predicate, + transform: ServiceDecoratedByProvider.TransformMultiple) + .SelectMany(static (decorations, _) => + decorations.ToImmutableArray()) // Flatten IEnumerable + .WithTrackingName(TrackingNames.Attr_ServiceDecoration_Transform) + .Where(static x => x is not null) + .WithTrackingName(TrackingNames.Attr_ServiceDecoration_FilterNotNull) + .Select(static (x, _) => x!.Value) + .WithTrackingName(TrackingNames.Attr_ServiceDecoration_Stream); + + // --- [SkipAssemblyDecorators] stream -------------------------------- + // Discovers implementations that have opted out of all assembly-level decorations. + // These markers are used to filter out assembly-level rules during the merge phase, + // while still allowing class-level decorations to be applied. + var skipAssemblyDecorations = context.SyntaxProvider + .ForAttributeWithMetadataName( + AttributeNames.SkipAssemblyDecorationAttribute, + predicate: SkipAssemblyDecoratorProvider.Predicate, + transform: SkipAssemblyDecoratorProvider.Transform) + .WithTrackingName(TrackingNames.Attr_SkipAssemblyDecoration_Transform) + .Where(static x => x is not null) + .WithTrackingName(TrackingNames.Attr_SkipAssemblyDecoration_FilterNotNull) + .Select(static (x, _) => x!.Value) + .WithTrackingName(TrackingNames.Attr_SkipAssemblyDecoration_Stream); + + // --- [DoNotDecorate(...)] stream --------------------------------- + // Discovers implementations that want to exclude specific decorators from the merged set. + // These directives are applied after deduplication but before final emission. + // Matching is by TypeDefId (definition only), so open generic decorator types work correctly. + var doNotDecorateDirectives = context.SyntaxProvider + .ForAttributeWithMetadataName( + AttributeNames.DoNotDecorateAttribute, + predicate: DoNotDecorateProvider.Predicate, + transform: DoNotDecorateProvider.TransformMultiple) + .SelectMany(static (directives, _) => + directives.ToImmutableArray()) // Flatten IEnumerable + .WithTrackingName(TrackingNames.Attr_DoNotDecorate_Transform) + .Where(static x => x is not null) + .WithTrackingName(TrackingNames.Attr_DoNotDecorate_FilterNotNull) + .Select(static (x, _) => x!.Value) + .WithTrackingName(TrackingNames.Attr_DoNotDecorate_Stream); + // ✅ Gate each VALUES stream before Collect() var genericGated = genericDecorations .Combine(csharpSufficient) @@ -68,11 +122,34 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select(static (pair, _) => pair.Left) .WithTrackingName(TrackingNames.Gate_Decorations_NonGeneric); - // Collect both decoration streams + var serviceGated = serviceDecorations + .Combine(csharpSufficient) + .Where(static pair => pair.Right) + .Select(static (pair, _) => pair.Left) + .WithTrackingName(TrackingNames.Gate_Decorations_Service); + + var skipAssemblyGated = skipAssemblyDecorations + .Combine(csharpSufficient) + .Where(static pair => pair.Right) + .Select(static (pair, _) => pair.Left) + .WithTrackingName(TrackingNames.Gate_Decorations_SkipAssembly); + + var doNotDecorateGated = doNotDecorateDirectives + .Combine(csharpSufficient) + .Where(static pair => pair.Right) + .Select(static (pair, _) => pair.Left) + .WithTrackingName(TrackingNames.Gate_Decorations_DoNotDecorate); + + // Collect class-level decorations (generic + non-generic combined) var allDecorations = genericGated.Collect() .Combine(nonGenericGated.Collect()) .WithTrackingName(TrackingNames.Attr_All_Combined); + // Collect assembly-level service decorations separately (to be matched against registrations) + var serviceDecosCollected = serviceGated.Collect() + .WithTrackingName(TrackingNames.Attr_Service_Collected); + + // --- Closed generic registration discovery ----------------------- var closedGenericRegs = context.SyntaxProvider .CreateSyntaxProvider( @@ -91,20 +168,60 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Collect() .WithTrackingName(TrackingNames.Reg_ClosedGeneric_Collect); + var skipAssemblyCollected = skipAssemblyGated + .Collect() + .WithTrackingName(TrackingNames.Reg_SkipAssembly_Collect); + + var doNotDecorateCollected = doNotDecorateGated + .Collect() + .WithTrackingName(TrackingNames.Reg_DoNotDecorate_Collect); + + // Combine all inputs using Select to create a clean named tuple structure + var allInputs = allDecorations + .Combine(serviceDecosCollected) + .Combine(skipAssemblyCollected) + .Combine(doNotDecorateCollected) + .Combine(closedGenericRegsCollected) + .Select(static (data, _) => ( + GenericDecos: data.Left.Left.Left.Left.Left, + NonGenericDecos: data.Left.Left.Left.Left.Right, + ServiceDecos: data.Left.Left.Left.Right, + SkipMarkers: data.Left.Left.Right, + DoNotDecorateDirectives: data.Left.Right, + Registrations: data.Right + )); + // Emit once we have decorations + registrations context.RegisterSourceOutput( - allDecorations.Combine(closedGenericRegsCollected) - .WithTrackingName(TrackingNames.Emit_ClosedGenericInterceptors), - static (spc, pair) => + allInputs.WithTrackingName(TrackingNames.Emit_ClosedGenericInterceptors), + (spc, inputs) => { - var (genericDecos, nonGenericDecos) = pair.Left; - var regs = pair.Right; + // Merge class-level decorations (generic + non-generic) + var classDecos = inputs.GenericDecos.Concat(inputs.NonGenericDecos).ToImmutableArray(); - var allDecos = genericDecos.AddRange(nonGenericDecos); - var byImpl = BuildDecorationMap(allDecos); // Dictionary> + // Build a fast lookup of implementations that opted out via [SkipAssemblyDecorators] + var skipped = new HashSet(inputs.SkipMarkers.Select(m => m.ImplementationDef)); + + // Convert ServiceDecoration → DecoratorToIntercept by matching service types with registrations + var assemblyDecos = new List(); + foreach (var matchingDecos in inputs.Registrations.Select(reg => inputs.ServiceDecos + .Where(sd => + sd.ServiceDef.Equals(reg.ServiceDef) && sd.AssemblyName == reg.ImplDef.AssemblyName) + .Where(_ => !skipped.Contains(reg.ImplDef)) + .Select(sd => new DecoratorToIntercept( + ImplementationDef: reg.ImplDef, + DecoratorDef: sd.DecoratorDef, + Order: sd.Order, + IsInterceptable: true)))) + { + assemblyDecos.AddRange(matchingDecos); + } + + // Build decoration map with proper merge/precedence logic (class-level and assembly-level kept separate) + var byImpl = BuildDecorationMap(classDecos, assemblyDecos.ToImmutableArray(), inputs.DoNotDecorateDirectives); // Only emit interceptors for registrations that have decorators - var regsWithDecorators = regs + var regsWithDecorators = inputs.Registrations .Where(r => byImpl.TryGetValue(r.ImplDef, out var decos) && decos.Count > 0) .ToEquatableArray(); @@ -121,24 +238,94 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } private static Dictionary> BuildDecorationMap( - ImmutableArray items) + ImmutableArray classDecorators, + ImmutableArray assemblyDecorators, + ImmutableArray doNotDecorateDirectives) { - var tmp = new Dictionary>(); - foreach (var d in items.Where(d => d.IsInterceptable)) + // Build intermediate structure: ImplementationDef -> List + var grouped = new Dictionary>(); + + // Add class-level decorators + foreach (var d in classDecorators.Where(d => d.IsInterceptable)) + { + if (!grouped.TryGetValue(d.ImplementationDef, out var list)) + grouped[d.ImplementationDef] = list = new(); + + list.Add(new MergedDecoration( + DecoratorDef: d.DecoratorDef, + Order: d.Order, + Source: DecorationSource.Class, + IsInterceptable: d.IsInterceptable)); + } + + // Add assembly-level decorators (always IsInterceptable: true for now) + foreach (var d in assemblyDecorators.Where(d => d.IsInterceptable)) + { + if (!grouped.TryGetValue(d.ImplementationDef, out var list)) + grouped[d.ImplementationDef] = list = new(); + + list.Add(new MergedDecoration( + DecoratorDef: d.DecoratorDef, + Order: d.Order, + Source: DecorationSource.Assembly, + IsInterceptable: true)); + } + + // Build fast lookup: ImplementationDef -> Set + var exclusions = new Dictionary>(); + foreach (var directive in doNotDecorateDirectives) { - if (!tmp.TryGetValue(d.ImplementationDef, out var list)) - tmp[d.ImplementationDef] = list = new(); + if (!exclusions.TryGetValue(directive.ImplementationDef, out var excludedSet)) + exclusions[directive.ImplementationDef] = excludedSet = new HashSet(); - list.Add((d.Order, d.DecoratorDef)); + excludedSet.Add(directive.DecoratorDef); } - var result = new Dictionary>(tmp.Count); - foreach (var (impl, list) in tmp) + // Deduplicate and sort each implementation's decorators + var result = new Dictionary>(); + foreach (var (impl, decorations) in grouped) { - list.Sort(static (a, b) => a.Order.CompareTo(b.Order)); - var unique = list.Select(x => x.Deco).Distinct(); - result[impl] = new EquatableArray(unique); + // Group by DecoratorDef to find duplicates, then take the one with lowest Source (Class=0 wins over Assembly=1) + var deduplicated = decorations + .GroupBy(m => m.DecoratorDef) + .Select(g => g.MinBy(m => m.Source)!) // Class (0) wins over Assembly (1) + .OrderBy(m => m.Order) + .ThenBy(m => m.Source) + .ThenBy(m => GetSortableTypeName(m.DecoratorDef)) + .ToList(); + + // Apply DoNotDecorateDirective filtering (Priority 3) + // Remove decorators whose DecoratorDef matches any exclusion for this implementation + if (exclusions.TryGetValue(impl, out var excludedDecorators)) + { + deduplicated = deduplicated + .Where(m => !excludedDecorators.Contains(m.DecoratorDef)) + .ToList(); + } + + // Extract just the DecoratorDef for emission + result[impl] = new EquatableArray(deduplicated.Select(m => m.DecoratorDef)); } + return result; } + + /// + /// Generates a stable fully-qualified name for a type definition, used as a tiebreaker + /// when sorting decorators with the same Order and Source. + /// + private static string GetSortableTypeName(TypeDefId typeDefId) + { + var parts = new List(); + + if (typeDefId.ContainingNamespaces.Count > 0) + parts.Add(string.Join(".", typeDefId.ContainingNamespaces)); + + if (typeDefId.ContainingTypes.Count > 0) + parts.Add(string.Join("+", typeDefId.ContainingTypes)); + + parts.Add(typeDefId.MetadataName); + + return string.Join(".", parts.Where(p => !string.IsNullOrEmpty(p))); + } } diff --git a/src/LayeredCraft.DecoWeaver.Generators/Model/DecoratorToIntercept.cs b/src/LayeredCraft.DecoWeaver.Generators/Model/DecoratorToIntercept.cs index 73e862a..7b5aa41 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Model/DecoratorToIntercept.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Model/DecoratorToIntercept.cs @@ -10,8 +10,7 @@ internal readonly record struct DecoratorToIntercept( TypeDefId ImplementationDef, // e.g., SqlRepository`1 TypeDefId DecoratorDef, // e.g., CachingRepository`1 int Order, // default 0 - bool IsInterceptable, // default true; if false, generator should ignore - LocationId Location + bool IsInterceptable ); /// Lifetime of a DI registration. public enum DiLifetime : byte { Transient, Scoped, Singleton } @@ -49,4 +48,57 @@ public readonly record struct RegistrationOccurrence( TypeId Implementation, bool WasGenericMethod, // true if AddX, false if AddX(..., typeof(...), typeof(...)) LocationId Location -); \ No newline at end of file +); + +/// +/// One assembly-level decoration rule discovered from +/// [assembly: DecorateService(typeof(Service<>), typeof(Decorator<>), order: ...)]. +/// Applies only within the declaring assembly. +/// +internal readonly record struct ServiceDecoration( + string AssemblyName, // e.g., "LayeredCraft.Data" + TypeDefId ServiceDef, // e.g., IRepository`1 + TypeDefId DecoratorDef, // e.g., CachingRepository`1 + int Order +); + +/// +/// Marks that a given implementation type has opted out of all assembly-level decorations +/// via [SkipAssemblyDecorators]. +/// +internal readonly record struct SkipAssemblyDecoratorsMarker( + TypeDefId ImplementationDef +); + +/// +/// Removes a specific decorator (by type definition) from the merged set for the given implementation, +/// discovered from [DoNotDecorate(typeof(Decorator<>))]. +/// +internal readonly record struct DoNotDecorateDirective( + TypeDefId ImplementationDef, // where the attribute appears + TypeDefId DecoratorDef // definition to remove (generic def allowed) +); + +/// +/// Origin of a decorator in the final merged chain (used for deterministic sorting and precedence). +/// +internal enum DecorationSource : byte +{ + /// Declared directly on the implementation (via [DecoratedBy]). + Class = 0, + + /// Declared at the assembly level (via [assembly: DecorateService]). + Assembly = 1 +} + +/// +/// Unified, already-merged view of a single decorator to emit for a specific registration +/// (service + implementation). Sorting is by Order asc, then Source asc (Class before Assembly), +/// then by a stable tiebreaker in the pipeline. +/// +internal readonly record struct MergedDecoration( + TypeDefId DecoratorDef, + int Order, + DecorationSource Source, + bool IsInterceptable // true for class-level unless explicitly disabled; assembly-level defaults to true +); diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs index 99bc288..e0a5614 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/ClosedGenericRegistrationProvider.cs @@ -68,8 +68,9 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) if (il is null) return null; // Generate fully qualified names for the closed types - var serviceFqn = $"global::{svc.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}"; - var implFqn = $"global::{impl.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted))}"; + // FullyQualifiedFormat includes global:: for ALL types including nested generic type arguments + var serviceFqn = svc.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var implFqn = impl.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); return new ClosedGenericRegistration( ServiceDef: svc.ToTypeId().Definition, diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByGenericProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByGenericProvider.cs index 42141d7..35e929a 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByGenericProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByGenericProvider.cs @@ -56,8 +56,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) ImplementationDef: implDef.ToTypeId().Definition, DecoratorDef: decoratorSym.ToTypeId().Definition, Order: order, - IsInterceptable: true, - Location: ctx.TargetNode.ToLocationId()); + IsInterceptable: true); } } } \ No newline at end of file diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByNonGenericProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByNonGenericProvider.cs index 10267b2..f67c77e 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByNonGenericProvider.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/DecoratedByNonGenericProvider.cs @@ -47,7 +47,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) // Order can be either ctor arg #1 (int) or named arg "Order" var order = AttributeHelpers.GetIntNamedArg(attr, "Order", defaultValue: 0); - if (order == 0 && attr.ConstructorArguments.Length >= 2 && attr.ConstructorArguments[1].Value is int ctorOrder) + if (order == 0 && attr.ConstructorArguments is [_, { Value: int ctorOrder } _, ..]) order = ctorOrder; // First ctor arg is the Type @@ -61,8 +61,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) ImplementationDef: implDef.ToTypeId().Definition, DecoratorDef: decoratorSym.ToTypeId().Definition, Order: order, - IsInterceptable: true, - Location: ctx.TargetNode.ToLocationId()); + IsInterceptable: true); } } } \ No newline at end of file diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/DoNotDecorateProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/DoNotDecorateProvider.cs new file mode 100644 index 0000000..9a06e1e --- /dev/null +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/DoNotDecorateProvider.cs @@ -0,0 +1,53 @@ +using DecoWeaver.Model; +using DecoWeaver.Roslyn; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DecoWeaver.Providers; + +/// +/// Discovers [DoNotDecorate(typeof(...))] attributes on implementation classes. +/// These directives exclude specific decorators from the merged decoration chain, +/// allowing fine-grained control over which decorators apply to each implementation. +/// +internal static class DoNotDecorateProvider +{ + /// + /// Filters to classes only (AttributeTargets.Class). ForAttributeWithMetadataName passes all + /// node types that could have attributes, so we pre-filter here to avoid semantic analysis on + /// structs, interfaces, enums, etc. Decorator pattern requires reference semantics (classes/records), + /// not value types (structs/record structs). + /// + internal static bool Predicate(SyntaxNode node, CancellationToken _) + => node is ClassDeclarationSyntax; + + /// + /// Processes all [DoNotDecorate] attributes on a class, yielding one DoNotDecorateDirective per attribute. + /// This allows multiple decorators to be excluded from the same implementation. + /// + internal static IEnumerable TransformMultiple( + GeneratorAttributeSyntaxContext ctx, + CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + if (ctx.TargetSymbol is not INamedTypeSymbol implSym) + yield break; + + // Process all [DoNotDecorate] attributes on this class (pre-filtered by ForAttributeWithMetadataName) + foreach (var attr in ctx.Attributes) + { + // First ctor arg is the Type to exclude + if (attr.ConstructorArguments.Length == 0 || attr.ConstructorArguments[0].Kind != TypedConstantKind.Type) + continue; + + var decoratorSym = (ITypeSymbol?)attr.ConstructorArguments[0].Value; + if (decoratorSym is null) + continue; + + yield return new DoNotDecorateDirective( + ImplementationDef: implSym.ToTypeId().Definition, + DecoratorDef: decoratorSym.ToTypeId().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 new file mode 100644 index 0000000..298623b --- /dev/null +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/ServiceDecoratedByProvider.cs @@ -0,0 +1,72 @@ +using DecoWeaver.Model; +using DecoWeaver.Roslyn; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DecoWeaver.Providers; + +internal static class ServiceDecoratedByProvider +{ + /// + /// Filters to classes only (AttributeTargets.Class). ForAttributeWithMetadataName passes all + /// node types that could have attributes, so we pre-filter here to avoid semantic analysis on + /// structs, interfaces, enums, etc. Decorator pattern requires reference semantics (classes/records), + /// not value types (structs/record structs). + /// + internal static bool Predicate(SyntaxNode node, CancellationToken _) + => node is CompilationUnitSyntax cu + && cu.AttributeLists.Any(al => + al.Target is { Identifier.ValueText: "assembly" or "module" }); + + /// + /// Processes all [DecorateService] attributes on an assembly, yielding one ServiceDecoration per attribute. + /// These describe decorators that should apply to all implementations of a service type within the assembly. + /// + internal static IEnumerable TransformMultiple(GeneratorAttributeSyntaxContext ctx, + CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + if (ctx.TargetSymbol is not IAssemblySymbol asm) + yield break; + + // Get the assembly name for scoping the decoration rules + var assemblyName = asm.Name; + + // Process all [DecorateService] attributes on this assembly (pre-filtered by ForAttributeWithMetadataName) + foreach (var attr in ctx.Attributes) + { + // Only process DecorateServiceAttribute with pattern matching for namespace + if (attr.AttributeClass is not + { + MetadataName: AttributeNames.ServiceDecoratedByMetadataName, + ContainingNamespace: + { + Name: "Attributes", + ContainingNamespace: { Name: "DecoWeaver", ContainingNamespace.IsGlobalNamespace: true } + } + }) + continue; + // Order can either ctor arg #2 (int) or named arg "Order" + var order = AttributeHelpers.GetIntNamedArg(attr, "Order", defaultValue: 0); + if (order == 0 && attr.ConstructorArguments is [_, _, { Value: int ctorOrder } _, ..]) + order = ctorOrder; + + // Constructor args: [0] = service type, [1] = decorator type + if (attr.ConstructorArguments.Length < 2 || + attr.ConstructorArguments[0].Kind != TypedConstantKind.Type || + attr.ConstructorArguments[1].Kind != TypedConstantKind.Type) + continue; + + var serviceSym = (ITypeSymbol?)attr.ConstructorArguments[0].Value; + var decoratorSym = (ITypeSymbol?)attr.ConstructorArguments[1].Value; + if (serviceSym is null || decoratorSym is null) continue; + + yield return new ServiceDecoration( + AssemblyName: assemblyName, + ServiceDef: serviceSym.ToTypeId().Definition, + DecoratorDef: decoratorSym.ToTypeId().Definition, + Order: order); + } + } +} \ No newline at end of file diff --git a/src/LayeredCraft.DecoWeaver.Generators/Providers/SkipAssemblyDecoratorProvider.cs b/src/LayeredCraft.DecoWeaver.Generators/Providers/SkipAssemblyDecoratorProvider.cs new file mode 100644 index 0000000..e1f3252 --- /dev/null +++ b/src/LayeredCraft.DecoWeaver.Generators/Providers/SkipAssemblyDecoratorProvider.cs @@ -0,0 +1,30 @@ +using DecoWeaver.Model; +using DecoWeaver.Roslyn; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DecoWeaver.Providers; + +internal static class SkipAssemblyDecoratorProvider +{ + /// + /// Filters to classes only (AttributeTargets.Class). ForAttributeWithMetadataName passes all + /// node types that could have attributes, so we pre-filter here to avoid semantic analysis on + /// structs, interfaces, enums, etc. Decorator pattern requires reference semantics (classes/records), + /// not value types (structs/record structs). + /// + internal static bool Predicate(SyntaxNode node, CancellationToken _) + => node is ClassDeclarationSyntax; + + internal static SkipAssemblyDecoratorsMarker? Transform(GeneratorAttributeSyntaxContext ctx, CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + if (ctx.TargetSymbol is not INamedTypeSymbol implDef) + return null; + + return new SkipAssemblyDecoratorsMarker( + ImplementationDef: implDef.ToTypeId().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 index a65bf9b..008825f 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/Roslyn/RoslynAdapters.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/Roslyn/RoslynAdapters.cs @@ -6,13 +6,6 @@ namespace DecoWeaver.Roslyn; internal static class RoslynAdapters { - public static LocationId ToLocationId(this SyntaxNode node) - { - var span = node.GetLocation().SourceSpan; - var path = node.SyntaxTree.FilePath; - return new(path, span.Start, span.Length); - } - public static TypeId ToTypeId(this ITypeSymbol t) { switch (t) diff --git a/src/LayeredCraft.DecoWeaver.Generators/TrackingNames.cs b/src/LayeredCraft.DecoWeaver.Generators/TrackingNames.cs index 5810b2e..0a9c3da 100644 --- a/src/LayeredCraft.DecoWeaver.Generators/TrackingNames.cs +++ b/src/LayeredCraft.DecoWeaver.Generators/TrackingNames.cs @@ -13,10 +13,20 @@ public static class TrackingNames public const string Attr_NonGeneric_Transform = nameof(Attr_NonGeneric_Transform); public const string Attr_Generic_FilterNotNull = nameof(Attr_Generic_FilterNotNull); public const string Attr_NonGeneric_FilterNotNull = nameof(Attr_NonGeneric_FilterNotNull); + public const string Attr_ServiceDecoration_Transform = nameof(Attr_ServiceDecoration_Transform); + public const string Attr_ServiceDecoration_FilterNotNull = nameof(Attr_ServiceDecoration_FilterNotNull); + public const string Attr_ServiceDecoration_Stream = nameof(Attr_ServiceDecoration_Stream); + public const string Attr_SkipAssemblyDecoration_Transform = nameof(Attr_SkipAssemblyDecoration_Transform); + public const string Attr_SkipAssemblyDecoration_FilterNotNull = nameof(Attr_SkipAssemblyDecoration_FilterNotNull); + public const string Attr_SkipAssemblyDecoration_Stream = nameof(Attr_SkipAssemblyDecoration_Stream); + public const string Attr_DoNotDecorate_Transform = nameof(Attr_DoNotDecorate_Transform); + public const string Attr_DoNotDecorate_FilterNotNull = nameof(Attr_DoNotDecorate_FilterNotNull); + public const string Attr_DoNotDecorate_Stream = nameof(Attr_DoNotDecorate_Stream); public const string Attr_Generic_Stream = nameof(Attr_Generic_Stream); public const string Attr_NonGeneric_Stream = nameof(Attr_NonGeneric_Stream); public const string Attr_All_Combined = nameof(Attr_All_Combined); + public const string Attr_Service_Collected = nameof(Attr_Service_Collected); // Gated flow public const string Gate_Decorations = nameof(Gate_Decorations); @@ -32,6 +42,8 @@ public static class TrackingNames public const string Reg_ClosedGeneric_Transform = nameof(Reg_ClosedGeneric_Transform); public const string Reg_ClosedGeneric_Filter = nameof(Reg_ClosedGeneric_Filter); public const string Reg_ClosedGeneric_Collect = nameof(Reg_ClosedGeneric_Collect); + public const string Reg_SkipAssembly_Collect = nameof(Reg_SkipAssembly_Collect); + public const string Reg_DoNotDecorate_Collect = nameof(Reg_DoNotDecorate_Collect); // Emission public const string Emit_ClosedGenericInterceptors = nameof(Emit_ClosedGenericInterceptors); @@ -39,4 +51,7 @@ public static class TrackingNames // Optional gate per-stream public const string Gate_Decorations_Generic = nameof(Gate_Decorations_Generic); public const string Gate_Decorations_NonGeneric = nameof(Gate_Decorations_NonGeneric); + public const string Gate_Decorations_Service = nameof(Gate_Decorations_Service); + public const string Gate_Decorations_SkipAssembly = nameof(Gate_Decorations_SkipAssembly); + public const string Gate_Decorations_DoNotDecorate = nameof(Gate_Decorations_DoNotDecorate); } \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/023_ServiceDecorator_SingleDecorator/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/023_ServiceDecorator_SingleDecorator/Global.cs new file mode 100644 index 0000000..47509e7 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/023_ServiceDecorator_SingleDecorator/Global.cs @@ -0,0 +1,3 @@ +using DecoWeaver.Sample; + +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/023_ServiceDecorator_SingleDecorator/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/023_ServiceDecorator_SingleDecorator/Program.cs new file mode 100644 index 0000000..a1e4c36 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/023_ServiceDecorator_SingleDecorator/Program.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; +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() + .AddScoped, DynamoDbRepository>() + .AddScoped, DynamoDbRepository>() + .BuildServiceProvider(); + +// Test with Customer repository +var customerRepo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {customerRepo.GetType().Name}"); +customerRepo.Save(new Customer { Id = 1, Name = "Test Customer" }); +Console.WriteLine(); + +// Test with Order repository +var orderRepo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {orderRepo.GetType().Name}"); +orderRepo.Save(new Order { Id = 1, Total = 99.99m }); + +public class Customer +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +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/023_ServiceDecorator_SingleDecorator/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/023_ServiceDecorator_SingleDecorator/Repository.cs new file mode 100644 index 0000000..41cfbd2 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/023_ServiceDecorator_SingleDecorator/Repository.cs @@ -0,0 +1,32 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T item); +} + +public sealed class DynamoDbRepository : IRepository +{ + public void Save(T item) + { + Console.WriteLine($"Saving in {nameof(DynamoDbRepository<>)}, type: {typeof(T).Name}"); + } +} + +public sealed class CachingRepository : IRepository +{ + private readonly IRepository _innerRepository; + + public CachingRepository(IRepository innerRepository) + { + _innerRepository = innerRepository; + } + + public void Save(T item) + { + Console.WriteLine("Saved item to cache."); + _innerRepository.Save(item); + } +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/024_ServiceDecorator_MultipleOrdered/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/024_ServiceDecorator_MultipleOrdered/Global.cs new file mode 100644 index 0000000..6061040 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/024_ServiceDecorator_MultipleOrdered/Global.cs @@ -0,0 +1,4 @@ +using DecoWeaver.Sample; + +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(CachingRepository<>), 1)] +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), 2)] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/024_ServiceDecorator_MultipleOrdered/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/024_ServiceDecorator_MultipleOrdered/Program.cs new file mode 100644 index 0000000..ff75f94 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/024_ServiceDecorator_MultipleOrdered/Program.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; +using DecoWeaver.Sample; + +// Test multiple decorators with explicit order +// Order 1 (CachingRepository) should be innermost +// Order 2 (LoggingRepository) should be outermost +var serviceProvider = new ServiceCollection() + .AddScoped, DynamoDbRepository>() + .AddScoped, DynamoDbRepository>() + .BuildServiceProvider(); + +// Test with Customer repository +var customerRepo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {customerRepo.GetType().Name}"); +customerRepo.Save(new Customer { Id = 1, Name = "Test Customer" }); +Console.WriteLine(); + +// Test with Order repository +var orderRepo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {orderRepo.GetType().Name}"); +orderRepo.Save(new Order { Id = 1, Total = 99.99m }); + +public class Customer +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +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/024_ServiceDecorator_MultipleOrdered/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/024_ServiceDecorator_MultipleOrdered/Repository.cs new file mode 100644 index 0000000..4917c2b --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/024_ServiceDecorator_MultipleOrdered/Repository.cs @@ -0,0 +1,48 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T item); +} + +public sealed class DynamoDbRepository : IRepository +{ + public void Save(T item) + { + Console.WriteLine($"Saving in {nameof(DynamoDbRepository<>)}, type: {typeof(T).Name}"); + } +} + +public sealed class CachingRepository : IRepository +{ + private readonly IRepository _innerRepository; + + public CachingRepository(IRepository innerRepository) + { + _innerRepository = innerRepository; + } + + public void Save(T item) + { + Console.WriteLine("Saved item to cache."); + _innerRepository.Save(item); + } +} + +public sealed class LoggingRepository : IRepository +{ + private readonly IRepository _innerRepository; + + public LoggingRepository(IRepository innerRepository) + { + _innerRepository = innerRepository; + } + + public void Save(T item) + { + Console.WriteLine("Logging save operation."); + _innerRepository.Save(item); + } +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/025_ServiceDecorator_WithSkipAssembly/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/025_ServiceDecorator_WithSkipAssembly/Global.cs new file mode 100644 index 0000000..47509e7 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/025_ServiceDecorator_WithSkipAssembly/Global.cs @@ -0,0 +1,3 @@ +using DecoWeaver.Sample; + +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/025_ServiceDecorator_WithSkipAssembly/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/025_ServiceDecorator_WithSkipAssembly/Program.cs new file mode 100644 index 0000000..3a3e4ee --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/025_ServiceDecorator_WithSkipAssembly/Program.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; +using DecoWeaver.Sample; + +// Test skip assembly does not decorate SqlRepository +var serviceProvider = new ServiceCollection() + .AddScoped, DynamoDbRepository>() + .AddScoped, SqlRepository>() + .BuildServiceProvider(); + +// Test with Customer repository +var customerRepo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {customerRepo.GetType().Name}"); +customerRepo.Save(new Customer { Id = 1, Name = "Test Customer" }); +Console.WriteLine(); + +// Test with Order repository +var orderRepo = serviceProvider.GetRequiredService>(); +Console.WriteLine($"Resolved: {orderRepo.GetType().Name}"); +orderRepo.Save(new Order { Id = 1, Total = 99.99m }); + +public class Customer +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +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/025_ServiceDecorator_WithSkipAssembly/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/025_ServiceDecorator_WithSkipAssembly/Repository.cs new file mode 100644 index 0000000..86c0e64 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/025_ServiceDecorator_WithSkipAssembly/Repository.cs @@ -0,0 +1,41 @@ +using DecoWeaver.Attributes; + +namespace DecoWeaver.Sample; + +public interface IRepository +{ + void Save(T item); +} + +public sealed class DynamoDbRepository : IRepository +{ + public void Save(T item) + { + Console.WriteLine($"Saving in {nameof(DynamoDbRepository<>)}, type: {typeof(T).Name}"); + } +} + +[SkipAssemblyDecoration] +public sealed class SqlRepository : IRepository +{ + public void Save(T item) + { + Console.WriteLine($"Saving in {nameof(SqlRepository<>)}, type: {typeof(T).Name}"); + } +} + +public sealed class CachingRepository : IRepository +{ + private readonly IRepository _innerRepository; + + public CachingRepository(IRepository innerRepository) + { + _innerRepository = innerRepository; + } + + public void Save(T item) + { + Console.WriteLine("Saved item to cache."); + _innerRepository.Save(item); + } +} diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/026_SkipAssemblyWithClassLevel/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/026_SkipAssemblyWithClassLevel/Global.cs new file mode 100644 index 0000000..50c3300 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/026_SkipAssemblyWithClassLevel/Global.cs @@ -0,0 +1,3 @@ +using DecoWeaver.Attributes; + +[assembly: DecorateService(typeof(IRepository<>), typeof(CachingRepository<>), order: 50)] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/026_SkipAssemblyWithClassLevel/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/026_SkipAssemblyWithClassLevel/Program.cs new file mode 100644 index 0000000..fd41225 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/026_SkipAssemblyWithClassLevel/Program.cs @@ -0,0 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Register SpecialRepository - should get ValidationRepository but NOT CachingRepository +services.AddScoped, SpecialRepository>(); + +public class User { } \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/026_SkipAssemblyWithClassLevel/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/026_SkipAssemblyWithClassLevel/Repository.cs new file mode 100644 index 0000000..fa506d5 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/026_SkipAssemblyWithClassLevel/Repository.cs @@ -0,0 +1,26 @@ +using DecoWeaver.Attributes; + +public interface IRepository +{ + void Save(T entity); +} + +// This repository opts out of assembly-level decorators (CachingRepository) +// but still has class-level decorators (ValidationRepository) that should apply +[SkipAssemblyDecoration] +[DecoratedBy(typeof(ValidationRepository<>), Order = 10)] +public sealed class SpecialRepository(IRepository inner) : IRepository +{ + public void Save(T entity) => inner.Save(entity); +} + +// Decorators +public sealed class CachingRepository(IRepository inner) : IRepository +{ + public void Save(T entity) => inner.Save(entity); +} + +public sealed class ValidationRepository(IRepository inner) : IRepository +{ + public void Save(T entity) => inner.Save(entity); +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/027_MergePrecedence_Deduplication/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/027_MergePrecedence_Deduplication/Global.cs new file mode 100644 index 0000000..05536af --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/027_MergePrecedence_Deduplication/Global.cs @@ -0,0 +1,5 @@ +using DecoWeaver.Sample; + +// Assembly-level: Caching@10 for IRepository<> and Logging@5 for IRepository<> +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(CachingRepository<>), 10)] +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), 5)] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/027_MergePrecedence_Deduplication/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/027_MergePrecedence_Deduplication/Program.cs new file mode 100644 index 0000000..3879492 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/027_MergePrecedence_Deduplication/Program.cs @@ -0,0 +1,10 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Two registrations to verify deduplication works independently per implementation +services.AddScoped, UserRepository>(); +services.AddScoped, ProductRepository>(); + +var provider = services.BuildServiceProvider(); \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/027_MergePrecedence_Deduplication/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/027_MergePrecedence_Deduplication/Repository.cs new file mode 100644 index 0000000..6818eb8 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/027_MergePrecedence_Deduplication/Repository.cs @@ -0,0 +1,45 @@ +namespace DecoWeaver.Sample; + +public interface IRepository +{ + T Get(string id); + void Save(T entity); +} + +public class User { } +public class Product { } + +// Decorator types +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + public CachingRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} + +public class LoggingRepository : IRepository +{ + private readonly IRepository _inner; + public LoggingRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} + +// UserRepository: Assembly declares Caching@10, class declares Caching@20 +// Expected: Deduplication - class-level wins, so only one CachingRepository at order 20 +[DecoWeaver.Attributes.DecoratedBy(typeof(CachingRepository<>), Order = 20)] +public class UserRepository : IRepository +{ + public User Get(string id) => throw new NotImplementedException(); + public void Save(User entity) => throw new NotImplementedException(); +} + +// ProductRepository: Assembly declares Logging@5, class declares Logging@15 +// Expected: Deduplication - class-level wins, so only one LoggingRepository at order 15 +[DecoWeaver.Attributes.DecoratedBy(typeof(LoggingRepository<>), Order = 15)] +public class ProductRepository : IRepository +{ + public Product Get(string id) => throw new NotImplementedException(); + public void Save(Product entity) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/028_MergePrecedence_SortOrder/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/028_MergePrecedence_SortOrder/Global.cs new file mode 100644 index 0000000..e4b7798 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/028_MergePrecedence_SortOrder/Global.cs @@ -0,0 +1,4 @@ +using DecoWeaver.Sample; + +// Assembly-level: Logging@10 for IRepository<> +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), 10)] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/028_MergePrecedence_SortOrder/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/028_MergePrecedence_SortOrder/Program.cs new file mode 100644 index 0000000..2d7b35c --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/028_MergePrecedence_SortOrder/Program.cs @@ -0,0 +1,8 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +services.AddScoped, UserRepository>(); + +var provider = services.BuildServiceProvider(); \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/028_MergePrecedence_SortOrder/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/028_MergePrecedence_SortOrder/Repository.cs new file mode 100644 index 0000000..73113af --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/028_MergePrecedence_SortOrder/Repository.cs @@ -0,0 +1,45 @@ +namespace DecoWeaver.Sample; + +public interface IRepository +{ + T Get(string id); + void Save(T entity); +} + +public class User { } + +// Decorator types +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + public CachingRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} + +public class LoggingRepository : IRepository +{ + private readonly IRepository _inner; + public LoggingRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} + +public class ValidationRepository : IRepository +{ + private readonly IRepository _inner; + public ValidationRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} + +// UserRepository: +// Assembly declares: Logging@10 +// Class declares: Validation@10 (same order as assembly-level Logging) +// Expected sort order: Validation@10 (Class, Source=0), then Logging@10 (Assembly, Source=1) +[DecoWeaver.Attributes.DecoratedBy(typeof(ValidationRepository<>), Order = 10)] +public class UserRepository : IRepository +{ + public User Get(string id) => throw new NotImplementedException(); + public void Save(User entity) => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Global.cs new file mode 100644 index 0000000..47509e7 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Global.cs @@ -0,0 +1,3 @@ +using DecoWeaver.Sample; + +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Program.cs new file mode 100644 index 0000000..12438cb --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Program.cs @@ -0,0 +1,12 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// DynamoDbRepository SHOULD generate interceptor with CachingRepository +services.AddScoped, DynamoDbRepository>(); + +// SqlRepository SHOULD NOT generate interceptor (DoNotDecorate applied) +services.AddScoped, SqlRepository>(); + +var provider = services.BuildServiceProvider(); \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Repository.cs new file mode 100644 index 0000000..60f6f8c --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Repository.cs @@ -0,0 +1,34 @@ +namespace DecoWeaver.Sample; + +public interface IRepository +{ + T Get(string id); + void Save(T entity); +} + +public class User { public int Id { get; set; } } +public class Order { public int Id { get; set; } } + +// DynamoDbRepository: Should get decorated (no DoNotDecorate) +public class DynamoDbRepository : IRepository +{ + public T Get(string id) => throw new NotImplementedException(); + public void Save(T entity) => throw new NotImplementedException(); +} + +// SqlRepository: Should NOT be decorated (DoNotDecorate applied) +[DecoWeaver.Attributes.DoNotDecorate(typeof(CachingRepository<>))] +public class SqlRepository : IRepository +{ + public T Get(string id) => throw new NotImplementedException(); + public void Save(T entity) => throw new NotImplementedException(); +} + +// Decorator +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + public CachingRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/030_DoNotDecorate_Multiple/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/030_DoNotDecorate_Multiple/Global.cs new file mode 100644 index 0000000..4788b53 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/030_DoNotDecorate_Multiple/Global.cs @@ -0,0 +1,6 @@ +using DecoWeaver.Sample; + +// Assembly declares 3 decorators for all IRepository<> implementations +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(CachingRepository<>), 10)] +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(LoggingRepository<>), 20)] +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(ValidationRepository<>), 30)] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/030_DoNotDecorate_Multiple/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/030_DoNotDecorate_Multiple/Program.cs new file mode 100644 index 0000000..f139f5b --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/030_DoNotDecorate_Multiple/Program.cs @@ -0,0 +1,9 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Should only have LoggingRepository (Caching and Validation excluded) +services.AddScoped, SqlRepository>(); + +var provider = services.BuildServiceProvider(); \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/030_DoNotDecorate_Multiple/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/030_DoNotDecorate_Multiple/Repository.cs new file mode 100644 index 0000000..2238ac2 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/030_DoNotDecorate_Multiple/Repository.cs @@ -0,0 +1,43 @@ +namespace DecoWeaver.Sample; + +public interface IRepository +{ + T Get(string id); + void Save(T entity); +} + +public class Order { public int Id { get; set; } } + +// SqlRepository: Opts out of Caching and Validation, keeps only Logging +[DecoWeaver.Attributes.DoNotDecorate(typeof(CachingRepository<>))] +[DecoWeaver.Attributes.DoNotDecorate(typeof(ValidationRepository<>))] +public class SqlRepository : IRepository +{ + public T Get(string id) => throw new NotImplementedException(); + public void Save(T entity) => throw new NotImplementedException(); +} + +// Decorators +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + public CachingRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} + +public class LoggingRepository : IRepository +{ + private readonly IRepository _inner; + public LoggingRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} + +public class ValidationRepository : IRepository +{ + private readonly IRepository _inner; + public ValidationRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/031_DoNotDecorate_OpenGenericMatching/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/031_DoNotDecorate_OpenGenericMatching/Global.cs new file mode 100644 index 0000000..47509e7 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/031_DoNotDecorate_OpenGenericMatching/Global.cs @@ -0,0 +1,3 @@ +using DecoWeaver.Sample; + +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/031_DoNotDecorate_OpenGenericMatching/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/031_DoNotDecorate_OpenGenericMatching/Program.cs new file mode 100644 index 0000000..f37ab9a --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/031_DoNotDecorate_OpenGenericMatching/Program.cs @@ -0,0 +1,9 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// Should NOT generate interceptor (open generic DoNotDecorate matches closed generic) +services.AddScoped, SqlRepository>(); + +var provider = services.BuildServiceProvider(); \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/031_DoNotDecorate_OpenGenericMatching/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/031_DoNotDecorate_OpenGenericMatching/Repository.cs new file mode 100644 index 0000000..15f7e15 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/031_DoNotDecorate_OpenGenericMatching/Repository.cs @@ -0,0 +1,26 @@ +namespace DecoWeaver.Sample; + +public interface IRepository +{ + T Get(string id); + void Save(T entity); +} + +public class Customer { public int Id { get; set; } } + +// SqlRepository: Uses open generic in DoNotDecorate to match all closed variants +// DoNotDecorate(typeof(CachingRepository<>)) should exclude CachingRepository +[DecoWeaver.Attributes.DoNotDecorate(typeof(CachingRepository<>))] +public class SqlRepository : IRepository +{ + public T Get(string id) => throw new NotImplementedException(); + public void Save(T entity) => throw new NotImplementedException(); +} + +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + public CachingRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _inner.Save(entity); +} \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/032_DoNotDecorate_IsolationCheck/Global.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/032_DoNotDecorate_IsolationCheck/Global.cs new file mode 100644 index 0000000..47509e7 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/032_DoNotDecorate_IsolationCheck/Global.cs @@ -0,0 +1,3 @@ +using DecoWeaver.Sample; + +[assembly: DecoWeaver.Attributes.DecorateService(typeof(IRepository<>), typeof(CachingRepository<>))] \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/032_DoNotDecorate_IsolationCheck/Program.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/032_DoNotDecorate_IsolationCheck/Program.cs new file mode 100644 index 0000000..3e494f3 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/032_DoNotDecorate_IsolationCheck/Program.cs @@ -0,0 +1,12 @@ +using DecoWeaver.Sample; +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); + +// SqlRepository: Should NOT generate interceptor (DoNotDecorate applied) +services.AddScoped, SqlRepository>(); + +// DynamoDbRepository: SHOULD generate interceptor with CachingRepository +services.AddScoped, DynamoDbRepository>(); + +var provider = services.BuildServiceProvider(); \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/032_DoNotDecorate_IsolationCheck/Repository.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/032_DoNotDecorate_IsolationCheck/Repository.cs new file mode 100644 index 0000000..378b54e --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Cases/032_DoNotDecorate_IsolationCheck/Repository.cs @@ -0,0 +1,33 @@ +namespace DecoWeaver.Sample; + +public interface IRepository +{ + T Get(string id); + void Save(T entity); +} + +public class Customer { public int Id { get; set; } } +public class Order { public int Id { get; set; } } + +// SqlRepository: Opts out of assembly-level Caching +[DecoWeaver.Attributes.DoNotDecorate(typeof(CachingRepository<>))] +public class SqlRepository : IRepository +{ + public T Get(string id) => throw new NotImplementedException(); + public void Save(T entity) => throw new NotImplementedException(); +} + +// DynamoDbRepository: Should still get decorated (DoNotDecorate doesn't affect it) +public class DynamoDbRepository : IRepository +{ + public T Get(string id) => throw new NotImplementedException(); + public void Save(T entity) => throw new NotImplementedException(); +} + +public class CachingRepository : IRepository +{ + private readonly IRepository _inner; + public CachingRepository(IRepository inner) => _inner = inner; + public T Get(string id) => _inner.Get(id); + public void Save(T entity) => _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 f613642..2933ce4 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/DecoWeaverGeneratorTests.cs @@ -276,4 +276,134 @@ await VerifyGlue.VerifySourcesAsync(sut, ], featureFlags: FeatureFlags); } + + [Theory] + [GeneratorAutoData] + public async Task ServiceDecorator_SingleDecorator_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/023_ServiceDecorator_SingleDecorator/Repository.cs", + "Cases/023_ServiceDecorator_SingleDecorator/Program.cs", + "Cases/023_ServiceDecorator_SingleDecorator/Global.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task ServiceDecorator_MultipleOrdered_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/024_ServiceDecorator_MultipleOrdered/Repository.cs", + "Cases/024_ServiceDecorator_MultipleOrdered/Program.cs", + "Cases/024_ServiceDecorator_MultipleOrdered/Global.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task ServiceDecorator_WithSkipAssembly_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/025_ServiceDecorator_WithSkipAssembly/Repository.cs", + "Cases/025_ServiceDecorator_WithSkipAssembly/Program.cs", + "Cases/025_ServiceDecorator_WithSkipAssembly/Global.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task ServiceDecorator_SkipAssemblyWithClassLevel_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/026_SkipAssemblyWithClassLevel/Repository.cs", + "Cases/026_SkipAssemblyWithClassLevel/Program.cs", + "Cases/026_SkipAssemblyWithClassLevel/Global.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task MergePrecedence_Deduplication_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/027_MergePrecedence_Deduplication/Repository.cs", + "Cases/027_MergePrecedence_Deduplication/Program.cs", + "Cases/027_MergePrecedence_Deduplication/Global.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task MergePrecedence_SortOrder_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/028_MergePrecedence_SortOrder/Repository.cs", + "Cases/028_MergePrecedence_SortOrder/Program.cs", + "Cases/028_MergePrecedence_SortOrder/Global.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task DoNotDecorate_RemovesAssemblyDecorator_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Repository.cs", + "Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Program.cs", + "Cases/029_DoNotDecorate_RemovesAssemblyDecorator/Global.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task DoNotDecorate_Multiple_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/030_DoNotDecorate_Multiple/Repository.cs", + "Cases/030_DoNotDecorate_Multiple/Program.cs", + "Cases/030_DoNotDecorate_Multiple/Global.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task DoNotDecorate_OpenGenericMatching_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/031_DoNotDecorate_OpenGenericMatching/Repository.cs", + "Cases/031_DoNotDecorate_OpenGenericMatching/Program.cs", + "Cases/031_DoNotDecorate_OpenGenericMatching/Global.cs" + ], + featureFlags: FeatureFlags); + } + + [Theory] + [GeneratorAutoData] + public async Task DoNotDecorate_IsolationCheck_GeneratesCorrectInterceptor(DecoWeaverGenerator sut) + { + await VerifyGlue.VerifySourcesAsync(sut, + [ + "Cases/032_DoNotDecorate_IsolationCheck/Repository.cs", + "Cases/032_DoNotDecorate_IsolationCheck/Program.cs", + "Cases/032_DoNotDecorate_IsolationCheck/Global.cs" + ], + featureFlags: FeatureFlags); + } } \ No newline at end of file diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_IsolationCheck_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_IsolationCheck_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..6b6522e --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_IsolationCheck_GeneratesCorrectInterceptor_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: "G5HC2ransGjLO8QFYSg2jVcBAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_0(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + + // 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.DoNotDecorate_Multiple_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_Multiple_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..2a0c99c --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_Multiple_GeneratesCorrectInterceptor_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: "+X+S9vDMobQA2GgxGn6/T8QAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.SqlRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_0(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.SqlRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.SqlRepository>(key); + + // 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.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.DoNotDecorate_RemovesAssemblyDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_RemovesAssemblyDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..e373fe2 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.DoNotDecorate_RemovesAssemblyDecorator_GeneratesCorrectInterceptor_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: "GBz4HIbdMdpW6und2FSh+MUAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_0(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + + // 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.MergePrecedence_Deduplication_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_Deduplication_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..22edee4 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_Deduplication_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,94 @@ +//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: "n1YVUPREqHtBvOzUa9Rd6NAAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.UserRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_0(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.UserRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.UserRepository>(key); + + // 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.LoggingRepository<>), current); + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.CachingRepository<>), current); + return current; + }); + return services; + } + + [InterceptsLocation(version: 1, data: "n1YVUPREqHtBvOzUa9Rd6AkBAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.ProductRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_1(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.ProductRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.ProductRepository>(key); + + // 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.MergePrecedence_SortOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_SortOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..60aa028 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.MergePrecedence_SortOrder_GeneratesCorrectInterceptor_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: "E4t/JRiF97jI61vwt4jak3wAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.UserRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_0(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.UserRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.UserRepository>(key); + + // 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.ValidationRepository<>), 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.OpenGeneric_ConstructorOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ConstructorOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index cf50c58..6765a7a 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ConstructorOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ConstructorOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,21 +21,21 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "qmMquO1J/cVrjFAdEDyPMwABAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddScoped_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddScoped>(sp => + services.AddScoped>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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.CachingRepository<>), current); return current; }); return services; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MixedOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MixedOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index a94bea9..02c048e 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MixedOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MixedOrderSyntax_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,22 +21,22 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "UriZ1pjNkKEQck5qWtHmvugAAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddScoped_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddScoped>(sp => + services.AddScoped>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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); + 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; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleDefaultOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleDefaultOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 395c377..dc96a5e 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleDefaultOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleDefaultOrder_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,22 +21,22 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "GjmnHhZH0uTodMXfmL/UVgABAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddScoped_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddScoped>(sp => + services.AddScoped>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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); + 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; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index d56aff1..0231a42 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,44 +21,44 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "kh94cO6krxrz8/UKmI0o21kBAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddScoped_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddScoped>(sp => + services.AddScoped>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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); + 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; } [InterceptsLocation(version: 1, data: "kh94cO6krxrz8/UKmI0o2xMBAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddScoped_1(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddScoped>(sp => + services.AddScoped>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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); + 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; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_NonGenericDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_NonGenericDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 9b46407..83caa20 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_NonGenericDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_NonGenericDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,21 +21,21 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "hdI0mb0oTCABU0WBVslSE/0AAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddScoped_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddScoped>(sp => + services.AddScoped>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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.AuditDecorator), current); + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.AuditDecorator), current); return current; }); return services; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 68760f3..2d81b68 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,42 +21,42 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "jlpBEu7DiUNh219Vtmk6h88BAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddScoped_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddScoped>(sp => + services.AddScoped>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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.CachingRepository<>), current); return current; }); return services; } [InterceptsLocation(version: 1, data: "jlpBEu7DiUNh219Vtmk6h4kBAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddScoped_1(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddScoped>(sp => + services.AddScoped>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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.CachingRepository<>), current); return current; }); return services; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ThreeDecorators_GeneratesCorrectNesting_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ThreeDecorators_GeneratesCorrectNesting_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index e120209..e44c2e3 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ThreeDecorators_GeneratesCorrectNesting_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.OpenGeneric_ThreeDecorators_GeneratesCorrectNesting_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,23 +21,23 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "3y5aHc1S+YQwrrEaSP2uWeIAAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddScoped_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddScoped>(sp => + services.AddScoped>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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.AuditRepository<>), current); - 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); + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.AuditRepository<>), current); + 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; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..0231a42 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_MultipleOrdered_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,94 @@ +//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: "kh94cO6krxrz8/UKmI0o21kBAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_0(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + + // 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; + } + + [InterceptsLocation(version: 1, data: "kh94cO6krxrz8/UKmI0o2xMBAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_1(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + + // 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.ServiceDecorator_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..2d81b68 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -0,0 +1,92 @@ +//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: "jlpBEu7DiUNh219Vtmk6h88BAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_0(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + + // 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; + } + + [InterceptsLocation(version: 1, data: "jlpBEu7DiUNh219Vtmk6h4kBAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_1(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + + // 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.ServiceDecorator_SkipAssemblyWithClassLevel_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SkipAssemblyWithClassLevel_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..b907efe --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_SkipAssemblyWithClassLevel_GeneratesCorrectInterceptor_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: "mT9RwGz5t9yYIeaAhDCX6L0AAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::IRepository, global::SpecialRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_0(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::IRepository), typeof(global::SpecialRepository)); + services.AddKeyedScoped, global::SpecialRepository>(key); + + // Register factory that applies decorators + services.AddScoped>(sp => + { + var current = (global::IRepository)sp.GetRequiredKeyedService>(key)!; + // Compose decorators (innermost to outermost) + current = (global::IRepository)DecoratorFactory.Create(sp, typeof(global::IRepository), typeof(global::ValidationRepository<>), 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.ServiceDecorator_WithSkipAssembly_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_WithSkipAssembly_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs new file mode 100644 index 0000000..8dcb1a5 --- /dev/null +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.ServiceDecorator_WithSkipAssembly_GeneratesCorrectInterceptor_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: "DRjvnpxnggdgCSpHkvbddrYAAABQcm9ncmFtLmNz")] + /// Intercepted: ServiceCollectionServiceExtensions.AddScoped<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + internal static IServiceCollection AddScoped_0(this IServiceCollection services) + where TService : class + where TImplementation : class, TService + { + // Register the undecorated implementation as a keyed service + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedScoped, global::DecoWeaver.Sample.DynamoDbRepository>(key); + + // 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.Singleton_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 51e7e5d..a356d52 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,22 +21,22 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "kWOXnTjy/FdPmBLHZ9t8S90AAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddSingleton<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddSingleton<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddSingleton_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedSingleton, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedSingleton, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddSingleton>(sp => + services.AddSingleton>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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); + 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; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 2f5052a..24949a2 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Singleton_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,21 +21,21 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "KffqUrVOuYV28Ypj+9zzjqcAAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddSingleton<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddSingleton<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddSingleton_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedSingleton, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedSingleton, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddSingleton>(sp => + services.AddSingleton>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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.LoggingRepository<>), current); + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.LoggingRepository<>), current); return current; }); return services; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 4502356..142fe0f 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_MultipleDecorators_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,22 +21,22 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "BfOlytAPVRF7x+iITUyhP90AAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddTransient<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddTransient<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddTransient_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedTransient, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedTransient, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddTransient>(sp => + services.AddTransient>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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); + 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; diff --git a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs index 7fda5ca..96a23d7 100644 --- a/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs +++ b/test/LayeredCraft.DecoWeaver.Generator.Tests/Snapshots/DecoWeaverGeneratorTests.Transient_SingleDecorator_GeneratesCorrectInterceptor_sut=DecoWeaver.DecoWeaverGenerator#DecoWeaver.Interceptors.ClosedGenerics.g.verified.cs @@ -21,21 +21,21 @@ namespace DecoWeaver.Generated file static class DecoWeaverInterceptors { [InterceptsLocation(version: 1, data: "BDjuVRZMlJ8bT8yo8cNScacAAABQcm9ncmFtLmNz")] - /// Intercepted: ServiceCollectionServiceExtensions.AddTransient<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) + /// Intercepted: ServiceCollectionServiceExtensions.AddTransient<global::DecoWeaver.Sample.IRepository, global::DecoWeaver.Sample.DynamoDbRepository>(IServiceCollection) internal static IServiceCollection AddTransient_0(this IServiceCollection services) where TService : class where TImplementation : class, TService { // Register the undecorated implementation as a keyed service - var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); - services.AddKeyedTransient, global::DecoWeaver.Sample.DynamoDbRepository>(key); + var key = DecoratorKeys.For(typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.DynamoDbRepository)); + services.AddKeyedTransient, global::DecoWeaver.Sample.DynamoDbRepository>(key); // Register factory that applies decorators - services.AddTransient>(sp => + services.AddTransient>(sp => { - var current = (global::DecoWeaver.Sample.IRepository)sp.GetRequiredKeyedService>(key)!; + 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.LoggingRepository<>), current); + current = (global::DecoWeaver.Sample.IRepository)DecoratorFactory.Create(sp, typeof(global::DecoWeaver.Sample.IRepository), typeof(global::DecoWeaver.Sample.LoggingRepository<>), current); return current; }); return services;