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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Source generator that helps register attribute marked services in the dependency
- Module method registration
- Duplicate Strategy - Skip,Replace,Append
- Registration Strategy - Self, Implemented Interfaces, Self With Interfaces
- Decorator registration (`RegisterDecorator`) — no runtime dependencies

### Usage

Expand All @@ -40,6 +41,7 @@ Place registration attribute on class. The class will be discovered and registe
- `[RegisterScoped]` Marks the class as a scoped service
- `[RegisterTransient]` Marks the class as a transient service
- `[RegisterServices]` Marks the method to be called to register services
- `[RegisterDecorator]` Marks the class as a decorator around an existing service

#### Attribute Properties

Expand Down Expand Up @@ -217,6 +219,119 @@ public class ServiceFactoryKeyed : IServiceKeyed
}
```

#### Decorators

Use the `RegisterDecorator` attribute to wrap an existing service registration without adding
any runtime dependencies. The generator emits all decoration helpers directly into the
consumer assembly.

Decorators inherit the lifetime of the service they decorate. Apply multiple decorators by
ordering them with the `Order` property — lower values are innermost (applied first), higher
values are outermost (applied last).

```c#
public interface IService { }

[RegisterSingleton<IService>]
public class Service : IService { }

[RegisterDecorator<IService>(Order = 1)]
public class LoggingDecorator : IService
{
public LoggingDecorator(IService inner) { }
}

[RegisterDecorator<IService>(Order = 2)]
public class CachingDecorator : IService
{
public CachingDecorator(IService inner) { }
}
```

Resolution order for the sample above: `CachingDecorator → LoggingDecorator → Service`.

##### Decorator Attribute Properties

| Property | Description |
|--------------------|------------------------------------------------------------------------------------------------|
| ServiceType | The type of service to decorate. Required unless the generic attribute form is used. |
| ImplementationType | The decorator type. If not set, the class the attribute is on will be used. |
| ServiceKey | Decorate a specific keyed registration. Requires .NET 8+ Microsoft.Extensions.DependencyInjection. |
| AnyKey | When `true`, decorate every keyed registration of `ServiceType` regardless of its key. |
| Factory | Name of a static factory method that builds the decorator. |
| Order | Ordering within the decoration chain. Lower = innermost. |
| Tags | Comma/semicolon-delimited list of registration tags. |

##### Keyed decoration

Decorate a single keyed variant, or use `AnyKey` to decorate them all:

```c#
[RegisterSingleton<IService>(ServiceKey = "alpha")]
public class AlphaService : IService { }

[RegisterDecorator<IService>(AnyKey = true)]
public class LoggingDecorator : IService
{
public LoggingDecorator(IService inner) { }
}
```

##### Factory-built decorators

Provide a static factory on the decorator class for complex construction:

```c#
[RegisterDecorator<IService>(Factory = nameof(Create))]
public class LoggingDecorator : IService
{
public LoggingDecorator(IService inner) { }

public static IService Create(IServiceProvider serviceProvider, IService inner)
=> new LoggingDecorator(inner);
}
```

For keyed decorators the factory takes an additional `object?` parameter for the key:

```c#
public static IService Create(IServiceProvider serviceProvider, object? serviceKey, IService inner)
=> new LoggingDecorator(inner);
```

##### Open-generic decoration

Open-generic decorators apply to every closed registration of the matching service type.
The generator supports decorating closed-generic registrations with an open-generic decorator
class; purely open-generic implementation registrations (e.g. `(IRepo<>, Repo<>)`) are not
decorated at runtime due to a Microsoft.Extensions.DependencyInjection limitation on factory
registrations for open generic service types.

```c#
public interface IRepo<T> { }

[RegisterSingleton<IRepo<string>, StringRepo>]
public class StringRepo : IRepo<string> { }

[RegisterDecorator(ServiceType = typeof(IRepo<>))]
public class LoggingRepo<T> : IRepo<T>
{
public LoggingRepo(IRepo<T> inner) { }
}
```

##### Tags

Decorators support the same tag-filtering as registrations:

```c#
[RegisterDecorator<IService>(Tags = "FrontEnd")]
public class FrontEndLoggingDecorator : IService
{
public FrontEndLoggingDecorator(IService inner) { }
}
```

#### Register Method

When the service registration is complex, use the `RegisterServices` attribute on a method that has a parameter of `IServiceCollection` or `ServiceCollection`
Expand Down
111 changes: 111 additions & 0 deletions src/Injectio.Attributes/RegisterDecoratorAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
namespace Injectio.Attributes;

/// <summary>
/// Attribute to indicate the target class should be registered as a decorator for an existing service.
/// The decorator wraps the previously registered service implementation and inherits its <see cref="P:Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Lifetime"/>.
/// </summary>
/// <example>Decorate <c>IService</c> with a logging wrapper
/// <code>
/// [RegisterDecorator(ServiceType = typeof(IService))]
/// public class LoggingDecorator : IService
/// {
/// public LoggingDecorator(IService inner) { }
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
[System.Diagnostics.Conditional("REGISTER_SERVICE_USAGES")]
public class RegisterDecoratorAttribute : Attribute
{
/// <summary>
/// The <see cref="Type"/> of the service to decorate.
/// </summary>
public Type? ServiceType { get; set; }

/// <summary>
/// The <see cref="Type"/> that implements the decorator. If not set, the class the attribute is on will be used.
/// </summary>
public Type? ImplementationType { get; set; }

/// <summary>
/// Gets or sets the key of the keyed service to decorate.
/// Leave unset (and <see cref="AnyKey"/> false) to decorate the non-keyed registration.
/// </summary>
public object? ServiceKey { get; set; }

/// <summary>
/// When <c>true</c>, the decorator is applied to every keyed registration of <see cref="ServiceType"/>,
/// regardless of its key. Equivalent to <c>KeyedService.AnyKey</c>.
/// </summary>
public bool AnyKey { get; set; }

/// <summary>
/// Name of a static factory method to construct the decorator.
/// </summary>
/// <remarks>
/// The factory signature must be <c>(IServiceProvider, TService) -&gt; TService</c> for non-keyed services
/// or <c>(IServiceProvider, object?, TService) -&gt; TService</c> for keyed services.
/// </remarks>
public string? Factory { get; set; }

/// <summary>
/// Gets or sets the order in which the decorator is applied. Lower values are applied first (innermost).
/// </summary>
public int Order { get; set; }

/// <summary>
/// Gets or sets the comma delimited list of registration tags.
/// </summary>
public string? Tags { get; set; }
}

#if NET7_0_OR_GREATER
/// <summary>
/// Attribute to indicate the target class should be registered as a decorator for <typeparamref name="TService"/>.
/// </summary>
/// <typeparam name="TService">The type of the service to decorate.</typeparam>
/// <example>
/// <code>
/// [RegisterDecorator&lt;IService&gt;]
/// public class LoggingDecorator : IService
/// {
/// public LoggingDecorator(IService inner) { }
/// }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
[System.Diagnostics.Conditional("REGISTER_SERVICE_USAGES")]
public class RegisterDecoratorAttribute<TService> : RegisterDecoratorAttribute
where TService : class
{
/// <summary>
/// Initializes a new instance of the <see cref="RegisterDecoratorAttribute{TService}"/> class.
/// </summary>
public RegisterDecoratorAttribute()
{
ServiceType = typeof(TService);
}
}

/// <summary>
/// Attribute to indicate the target class should be registered as a decorator for <typeparamref name="TService"/>
/// using <typeparamref name="TImplementation"/> as the decorator implementation.
/// </summary>
/// <typeparam name="TService">The type of the service to decorate.</typeparam>
/// <typeparam name="TImplementation">The type of the decorator implementation.</typeparam>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
[System.Diagnostics.Conditional("REGISTER_SERVICE_USAGES")]
public class RegisterDecoratorAttribute<TService, TImplementation> : RegisterDecoratorAttribute<TService>
where TService : class
where TImplementation : class, TService
{
/// <summary>
/// Initializes a new instance of the <see cref="RegisterDecoratorAttribute{TService, TImplementation}"/> class.
/// </summary>
public RegisterDecoratorAttribute()
{
ServiceType = typeof(TService);
ImplementationType = typeof(TImplementation);
}
}
#endif
7 changes: 7 additions & 0 deletions src/Injectio.Generators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ INJ0006 | Usage | Warning | Factory method has invalid signature
INJ0007 | Usage | Warning | Implementation does not implement service type
INJ0008 | Usage | Warning | Implementation type is abstract
INJ0009 | Usage | Warning | RegisterServices on non-static method in abstract class
INJ0010 | Usage | Warning | Decorator does not implement service type
INJ0011 | Usage | Warning | Decorator is missing service type
INJ0012 | Usage | Warning | Decorator has no constructor accepting the inner service
INJ0013 | Usage | Warning | Decorator factory method not found
INJ0014 | Usage | Warning | Decorator factory method has invalid signature
INJ0015 | Usage | Warning | Keyed decoration is not supported for open-generic services
INJ0016 | Usage | Warning | Decorator target service is not registered in this compilation
12 changes: 12 additions & 0 deletions src/Injectio.Generators/DecoratorRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Injectio.Generators;

public record DecoratorRegistration(
string DecoratorType,
string ServiceType,
string? ServiceKey,
bool IsAnyKey,
string? Factory,
int Order,
EquatableArray<string> Tags,
bool IsOpenGeneric = false
);
65 changes: 65 additions & 0 deletions src/Injectio.Generators/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,69 @@ public static class DiagnosticDescriptors
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor DecoratorDoesNotImplementService = new(
id: "INJ0010",
title: "Decorator does not implement service type",
messageFormat: "Decorator '{0}' does not implement or inherit from service type '{1}'",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor DecoratorMissingServiceType = new(
id: "INJ0011",
title: "Decorator is missing service type",
messageFormat: "Decorator '{0}' must specify a ServiceType either via the generic attribute or the ServiceType property",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor DecoratorMissingInnerConstructor = new(
id: "INJ0012",
title: "Decorator has no constructor accepting the inner service",
messageFormat: "Decorator '{0}' must expose a public constructor whose first parameter is of type '{1}' (or use Factory)",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor DecoratorFactoryNotFound = new(
id: "INJ0013",
title: "Decorator factory method not found",
messageFormat: "Decorator factory method '{0}' was not found on type '{1}'",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor DecoratorFactoryInvalidSignature = new(
id: "INJ0014",
title: "Decorator factory method has invalid signature",
messageFormat: "Decorator factory method '{0}' on type '{1}' must be static and accept (IServiceProvider, TService) for non-keyed or (IServiceProvider, object?, TService) for keyed decorators",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor DecoratorOpenGenericKeyed = new(
id: "INJ0015",
title: "Keyed decoration is not supported for open-generic services",
messageFormat: "Decorator '{0}' targets open-generic service '{1}' and cannot be used with ServiceKey or AnyKey",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
customTags: [WellKnownDiagnosticTags.CompilationEnd]
);

public static readonly DiagnosticDescriptor DecoratorTargetNotRegistered = new(
id: "INJ0016",
title: "Decorator target service is not registered in this compilation",
messageFormat: "Decorator '{0}' targets service '{1}' but no matching registration was found; decoration will be skipped at runtime if the service is not registered elsewhere",
category: Category,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
customTags: [WellKnownDiagnosticTags.CompilationEnd]
);
}
4 changes: 4 additions & 0 deletions src/Injectio.Generators/KnownTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public static class KnownTypes
public const string ModuleAttributeTypeName = $"{ModuleAttributeShortName}Attribute";
public const string ModuleAttributeFullName = $"{AbstractionNamespace}.{ModuleAttributeTypeName}";

public const string DecoratorAttributeShortName = "RegisterDecorator";
public const string DecoratorAttributeTypeName = $"{DecoratorAttributeShortName}Attribute";
public const string DecoratorAttributeFullName = $"{AbstractionNamespace}.{DecoratorAttributeTypeName}";


public const string ServiceLifetimeSingletonShortName = "Singleton";
public const string ServiceLifetimeSingletonTypeName = $"ServiceLifetime.{ServiceLifetimeSingletonShortName}";
Expand Down
Loading
Loading