![ChatGPT Image Sep 26, 2025, 08_53_38 AM (1)]()
Explore how to automate service registration in .NET using custom attributes. Learn to define injection scopes, scan assemblies dynamically, and reduce boilerplate code for a cleaner, maintainable, and scalable application architecture.
Managing dependency injection (DI) in large .NET applications can become repetitive and error-prone. Typically, developers register services manually in Startup.cs
or Program.cs
using statements like:
services.AddScoped<IOrderService, OrderService>();
services.AddSingleton<ICacheService, CacheService>();
While this is straightforward, as your application grows, keeping track of all registrations can become cumbersome. One way to simplify this is by automating DI registration using custom attributes and reflection.
The Concept
The idea is simple:
Define a custom attribute that specifies the DI lifetime (Scoped
, Singleton
, Transient
).
Attach the attribute to your interfaces.
During application startup, scan assemblies using reflection, find all interfaces marked with the attribute, locate their implementations, and register them in the DI container automatically.
This approach reduces boilerplate and enforces a convention-based registration strategy.
Step 1. Define a Custom Attribute
We create an InjectAttribute
to specify the lifetime:
[AttributeUsage(AttributeTargets.Interface)]
public class InjectAttribute : Attribute
{
public ServiceLifetime Lifetime { get; }
public InjectAttribute(ServiceLifetime lifetime)
{
Lifetime = lifetime;
}
}
Step 2. Apply Attribute on Interfaces
Decorate your interfaces with the lifetime information:
[Inject(ServiceLifetime.Scoped)]
public interface IOrderService
{
void ProcessOrder();
}
[Inject(ServiceLifetime.Singleton)]
public interface ICacheService
{
void CacheItem(string key, object value);
}
Step 3. Implement the Interfaces
public class OrderService : IOrderService
{
public void ProcessOrder() { /* implementation */ }
}
public class CacheService : ICacheService
{
public void CacheItem(string key, object value) { /* implementation */ }
}
Step 4. Automate Registration with Reflection
Using reflection, we scan assemblies and register services based on the attribute:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddServicesByAttribute(this IServiceCollection services)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
var interfaces = assembly.GetTypes()
.Where(t => t.IsInterface && t.GetCustomAttribute<InjectAttribute>() != null);
foreach (var iface in interfaces)
{
var attr = iface.GetCustomAttribute<InjectAttribute>();
var impl = assembly.GetTypes()
.FirstOrDefault(t => t.IsClass && !t.IsAbstract && iface.IsAssignableFrom(t));
if (impl == null) continue;
switch (attr.Lifetime)
{
case ServiceLifetime.Singleton:
services.AddSingleton(iface, impl);
break;
case ServiceLifetime.Scoped:
services.AddScoped(iface, impl);
break;
case ServiceLifetime.Transient:
services.AddTransient(iface, impl);
break;
}
}
}
return services;
}
}
Step 5. Register in Startup or Program
builder.Services.AddServicesByAttribute();
Now all interfaces marked with InjectAttribute
are automatically registered with their specified lifetime.
Benefits
Less Boilerplate: Automatically registers services—no manual wiring.
Clear Scope Control: Define Singleton, Scoped, or Transient directly on the class.
Easier Maintenance: Add new services with a simple attribute.
Automatic Discovery: Scans assemblies so no service is missed.
Cleaner Architecture: Keeps DI setup separate from business logic.
Scalable & Reliable: Adds new modules effortlessly and reduces registration errors.
Considerations
This approach scans all loaded assemblies, which can impact startup time; filtering to project-specific assemblies is recommended.
Currently, it selects the first implementation for each interface. Handling multiple implementations requires extra logic.
Open generics, decorators, or complex DI scenarios require additional handling.
Conclusion
Using a custom attribute-driven DI registration allows developers to maintain a clean and scalable codebase while automating repetitive DI tasks. This approach balances convention with flexibility and can be extended to fit complex enterprise architectures.