Dependency Injection
The Cratis.DependencyInjection namespace provides convention-based dependency injection features that simplify service registration in .NET applications. These utilities help reduce boilerplate code when working with Microsoft's dependency injection container.
Overview
The dependency injection utilities offer:
- Convention-based registration: Automatically register services following naming conventions
- Self-binding: Register concrete types for direct injection
- Lifecycle management: Control service lifetimes using attributes
- Selective registration: Exclude types from convention-based registration
Extension Methods
AddBindingsByConvention
Automatically registers services based on interface naming conventions.
services.AddBindingsByConvention();
Convention Rules:
- Interface and implementation must be in the same namespace
- Interface name must be
I{ImplementationName}(e.g.,IUserService→UserService) - Only one implementation per interface is allowed
- Types with
[IgnoreConvention]attribute are excluded
Examples:
// These will be automatically registered:
public interface IUserService { }
public class UserService : IUserService { } // Registered as Transient
public interface IOrderProcessor { }
[Singleton]
public class OrderProcessor : IOrderProcessor { } // Registered as Singleton
// This will be ignored:
[IgnoreConvention]
public class TestUserService : IUserService { }
AddSelfBindings
Registers concrete types for direct injection without requiring an interface.
services.AddSelfBindings();
Registration Rules:
- Non-static, non-abstract, non-interface types
- Types not derived from
Exception - Types without
[IgnoreConvention]attribute - Types not in
SystemorMicrosoftnamespaces - Types with resolvable constructor parameters
Examples:
// These will be automatically registered:
public class EmailSender { } // Registered as Transient
[Singleton]
public class CacheManager { } // Registered as Singleton
// These will be ignored:
public class SystemType { } // In System namespace
[IgnoreConvention]
public class InternalService { } // Has ignore attribute
public class BadService(string connectionString) { } // Primitive parameter
Attributes
SingletonAttribute
Marks a class to be registered as a singleton service.
[Singleton]
public class DatabaseConnection : IDatabaseConnection
{
// This service will be registered as a singleton
}
Usage:
- Apply to class declarations
- Works with both convention-based and self-binding registration
- Overrides the default transient lifetime
IgnoreConventionAttribute
Excludes a class from convention-based registration.
[IgnoreConvention]
public class TestImplementation : IService
{
// This class will not be automatically registered
}
Usage:
- Apply to class declarations
- Useful for test implementations, decorators, or manually registered services
- Affects both
AddBindingsByConventionandAddSelfBindings
Best Practices
1. Use Convention-Based Registration First
Start with convention-based registration for most services:
public void ConfigureServices(IServiceCollection services)
{
services.AddBindingsByConvention();
services.AddSelfBindings();
// Manual registrations for special cases
services.AddScoped<ISpecialService, CustomImplementation>();
}
2. Follow Naming Conventions
Ensure your interfaces and implementations follow the expected patterns:
// ✅ Good - follows convention
public interface IUserRepository { }
public class UserRepository : IUserRepository { }
// ❌ Bad - doesn't follow convention
public interface UserRepo { }
public class UserRepositoryImpl : UserRepo { }
3. Use Attributes Appropriately
Apply lifecycle and exclusion attributes where needed:
// Singleton for expensive-to-create services
[Singleton]
public class DatabaseConnectionPool : IDatabaseConnectionPool { }
// Ignore for test doubles or special implementations
[IgnoreConvention]
public class MockUserService : IUserService { }
4. Avoid Primitive Dependencies
Constructor parameters should use concepts or dependency injection:
// ❌ Bad - primitive parameters prevent auto-registration
public class EmailService(string smtpServer, int port) { }
// ✅ Good - uses concepts and injection
public class EmailService(SmtpConfiguration config, ILogger<EmailService> logger) { }
Integration Example
Complete setup in a typical ASP.NET Core application:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Add convention-based services
services.AddBindingsByConvention();
services.AddSelfBindings();
// Add framework services
services.AddControllers();
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(connectionString));
// Manual registrations for complex scenarios
services.AddScoped<IEmailService>(provider =>
new EmailService(provider.GetRequiredService<IConfiguration>()));
}
}
This approach significantly reduces the amount of manual service registration code while maintaining flexibility for special cases.