Description
Background and motivation
Dependency Injection (DI) is a core feature of modern .NET applications, but manually registering services can be repetitive and error-prone. This proposal introduces a set of attributes (InjectableSingleton
, InjectableScoped
, InjectableTransient
) and an extension method (AddInjectables
) to automate service registration while maintaining compatibility with existing DI patterns.
By using these attributes, developers can declaratively specify how a class should be registered in the DI container, reducing boilerplate code and improving readability.
API Proposal
1. Base Attribute: InjectableAttribute
We define a base attribute InjectableAttribute
that encapsulates common behavior for all injectable types.
[AttributeUsage(AttributeTargets.Class)]
public abstract class InjectableAttribute : Attribute
{
/// <summary>
/// Specifies the service lifetime.
/// </summary>
public ServiceLifetime Lifetime { get; }
/// <summary>
/// Optional array of service types for explicit registration.
/// </summary>
public Type[]? RegisterTypes { get; set; }
protected InjectableAttribute(ServiceLifetime lifetime)
{
Lifetime = lifetime;
}
}
2. Derived Attributes: InjectableSingleton
, InjectableScoped
, InjectableTransient
These attributes inherit from InjectableAttribute
and provide specific lifetimes. They allow developers to use familiar naming conventions while leveraging the flexibility of the base attribute.
public class InjectableSingletonAttribute : InjectableAttribute
{
public InjectableSingletonAttribute() : base(ServiceLifetime.Singleton) { }
}
public class InjectableScopedAttribute : InjectableAttribute
{
public InjectableScopedAttribute() : base(ServiceLifetime.Scoped) { }
}
public class InjectableTransientAttribute : InjectableAttribute
{
public InjectableTransientAttribute() : base(ServiceLifetime.Transient) { }
}
3. Extension Method: AddInjectables
This method scans assemblies for classes decorated with InjectableAttribute
and registers them in the DI container based on their specified metadata.
public static class ServiceCollectionExtensions
{
/// <summary>
/// Automatically registers services marked with [Injectable*] attributes from the calling assembly.
/// </summary>
public static IServiceCollection AddInjectables(this IServiceCollection services) =>
AddInjectables(services, Assembly.GetCallingAssembly());
/// <summary>
/// Automatically registers services marked with [Injectable*] attributes from a specified assembly.
/// </summary>
public static IServiceCollection AddInjectables(this IServiceCollection services, Assembly assembly)
{
var types = assembly.GetTypes();
foreach (var type in types)
{
var attr = type.GetCustomAttribute<InjectableAttribute>();
if (attr is null || !type.IsClass || type.IsAbstract)
{
continue;
}
var lifetime = attr.Lifetime;
var registerTypes = attr.RegisterTypes;
// Default to self-registration if no RegisterTypes are specified
if (registerTypes is null || registerTypes.Length == 0)
{
registerTypes = new[] { type };
}
if (registerTypes.Length == 1)
{
services.Add(new ServiceDescriptor(registerTypes[0], type, lifetime));
}
else
{
// Register the implementation type first
services.Add(new ServiceDescriptor(type, type, lifetime));
// Register each interface or self-type
foreach (var regType in registerTypes)
{
if (regType != type)
{
services.Add(new ServiceDescriptor(regType, sp => sp.GetRequiredService(type), lifetime));
}
}
}
}
return services;
}
}
Notice that we have services.Add(new ServiceDescriptor(type, type, lifetime));
where we register the service itself. This is likely not what we want in most cases. However, if the registered type implements multiple interfaces, we have no choice but to do so.
API Usage
Marking Classes for Registration
// Registered as Singleton, defaulting to self-registration
[InjectableSingleton]
public class MySingletonService { }
// Registered as Scoped, explicitly specifying interfaces
[InjectableScoped(RegisterTypes = new[] { typeof(IServiceA), typeof(IServiceB) })]
public class MyScopedService : IServiceA, IServiceB { }
// Registered as Transient, defaulting to self-registration
[InjectableTransient]
public class MyTransientService { }
Using in Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Automatically register all services marked with [Injectable*] in the current assembly
services.AddInjectables();
// Or register services from a specific assembly
services.AddInjectables(typeof(SomeOtherService).Assembly);
}
}
Alternative Designs
No response
Risks
Performance Impact: Scanning large assemblies may affect startup time.