Accepted
Modern .NET applications rely heavily on Dependency Injection (DI) for:
- Loose Coupling: Components depend on abstractions, not concrete types
- Testability: Easy to substitute implementations with mocks/fakes
- Configuration: Services configured centrally, not scattered throughout code
- Lifetime Management: Framework manages object creation and disposal
- Cross-Cutting Concerns: Decorators, interceptors, middleware
- Module Composition: Pluggable modules with isolated registrations
However, improper DI usage leads to:
- Memory Leaks: Capturing scoped dependencies in singletons
- Concurrency Issues: Shared mutable state in singleton services
- ObjectDisposedException: Using disposed scoped services
- Performance Degradation: Creating expensive objects too frequently
- Complexity: Service resolution failures difficult to diagnose
The application needed a DI strategy that:
- Defines clear lifetime rules (singleton, scoped, transient)
- Organizes registrations per module for modularity
- Prevents captive dependencies (scoped in singleton)
- Uses factory patterns where lifetime management is complex
- Supports decorator patterns (repository behaviors)
- Integrates with bITdevKit module infrastructure
Adopt Microsoft.Extensions.DependencyInjection with module-based registration, explicit lifetime choices, factory pattern for scoped dependencies in singletons, and decorator chains for cross-cutting concerns.
| Lifetime | When to Use | Examples |
|---|---|---|
| Singleton | Stateless services shared across application lifetime | Configuration, mappers, clients (if thread-safe), caches |
| Scoped | Services tied to request/operation scope | DbContext, repositories, request handlers, UnitOfWork |
| Transient | Lightweight, stateless services created per usage | Validators, specifications, value object factories |
public class CoreModule : WebModuleBase
{
public override IServiceCollection Register(
IServiceCollection services,
IConfiguration configuration = null,
IWebHostEnvironment environment = null)
{
// Configuration
var moduleConfiguration = this.Configure<CoreModuleConfiguration>(services, configuration);
// Domain Services (usually scoped or transient)
services.AddScoped<ICustomerDomainService, CustomerDomainService>();
// Application Services (handlers registered by MediatR)
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(
typeof(CoreModule).Assembly));
// Infrastructure - Database
services.AddSqlServerDbContext<CoreDbContext>(o => o
.UseConnectionString(moduleConfiguration.ConnectionStrings["Default"])
.UseLogger(true, System.Diagnostics.Tracing.EventLevel.Warning));
// Infrastructure - Repositories with Behaviors
services.AddEntityFrameworkRepository<Customer, CoreDbContext>()
.WithBehavior<RepositoryLoggingBehavior<Customer>>()
.WithBehavior<RepositoryDomainEventPublisherBehavior<Customer>>()
.WithBehavior<RepositoryAuditStateBehavior<Customer>>();
// Presentation - Endpoints
services.AddEndpoints<CustomerEndpoints>();
// Presentation - Mapping
services.AddMapping().WithMapster<CoreModuleMapperRegister>();
// Jobs
services.AddJobScheduling(o => o
.StartupDelay(configuration["JobScheduling:StartupDelay"]), configuration)
.WithJob<CustomerExportJob>()
.Cron(CronExpressions.EveryMinute)
.Named($"{this.Name}_{nameof(CustomerExportJob)}")
.RegisterScoped();
return services;
}
}// Configuration objects (immutable after bind)
services.AddSingleton<CoreModuleConfiguration>(sp =>
configuration.GetSection("Modules:Core").Get<CoreModuleConfiguration>());
// Mapster mapper (stateless after configuration)
services.AddSingleton<IMapper>(sp =>
new Mapper(TypeAdapterConfig.GlobalSettings));
// HttpClient factory (thread-safe)
services.AddHttpClient<IExternalApiClient, ExternalApiClient>();
// Caches (thread-safe implementations)
services.AddSingleton<IMemoryCache, MemoryCache>();// DbContext (NOT thread-safe, must be scoped)
services.AddDbContext<CoreDbContext>(options =>
options.UseSqlServer(connectionString), ServiceLifetime.Scoped);
// Repositories (depend on scoped DbContext)
services.AddScoped<IGenericRepository<Customer>, EntityFrameworkRepository<Customer, CoreDbContext>>();
// Request Handlers (operate on single request)
services.AddScoped<IRequestHandler<CustomerCreateCommand, Result<CustomerId>>, CustomerCreateCommandHandler>();
// Unit of Work (transaction scope)
services.AddScoped<IUnitOfWork, UnitOfWork>();// Validators (lightweight, stateless)
services.AddTransient<IValidator<CustomerCreateCommand>, CustomerCreateCommand.Validator>();
// Specifications (query building objects)
services.AddTransient<ISpecification<Customer>, CustomerEmailSpecification>();
// Value Object Factories (creation logic)
services.AddTransient<IEmailAddressFactory, EmailAddressFactory>();// WRONG: Captive dependency (scoped in singleton)
public class CustomerExportJob : JobBase
{
private readonly IGenericRepository<Customer> repository; // SCOPED!
public CustomerExportJob(IGenericRepository<Customer> repository) // Injected into SINGLETON job
{
this.repository = repository; // ObjectDisposedException when scope ends
}
}
// Correct: Use IServiceScopeFactory
[DisallowConcurrentExecution]
public class CustomerExportJob(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory) : JobBase(loggerFactory)
{
public override async Task Process(
IJobExecutionContext context,
CancellationToken cancellationToken)
{
using var scope = scopeFactory.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<IGenericRepository<Customer>>();
// Use repository within scope
var result = await repository.FindAllResultAsync(cancellationToken: cancellationToken);
}
}// Register repository with chained decorators
services.AddEntityFrameworkRepository<Customer, CoreDbContext>()
.WithBehavior<RepositoryLoggingBehavior<Customer>>() // Logs all operations
.WithBehavior<RepositoryDomainEventPublisherBehavior<Customer>>() // Publishes domain events
.WithBehavior<RepositoryAuditStateBehavior<Customer>>(); // Sets audit fields
// Execution order (onion pattern):
// Request → Logging → DomainEvents → Audit → Actual Repository// Register generic handler for all request types
services.AddScoped(typeof(IRequestHandler<,>), typeof(GenericCommandHandler<,>));
// Register generic repository for all entity types
services.AddScoped(typeof(IGenericRepository<>), typeof(EntityFrameworkRepository<>));
// Register generic validator for all command types
services.AddTransient(typeof(IValidator<>), typeof(FluentValidationValidator<>));- Built-in: Part of .NET, no additional dependencies
- Standards-Based: Industry-standard DI container
- Performance: Optimized for ASP.NET Core scenarios
- Tooling: First-class support in Visual Studio, analyzers
- Ecosystem: Works with most .NET libraries
- Simplicity: Good balance of features vs. complexity
- Cohesion: Services registered alongside related components
- Isolation: Each module manages its own dependencies
- Maintainability: Changes scoped to single module
- Testability: Modules can be registered independently in tests
- Discovery: Easy to find where services are registered
- Predictability: Clear when objects are created/disposed
- Performance: Avoid unnecessary object creation
- Safety: Prevents captive dependencies
- Documentation: Lifetime expresses intent
- Correctness: Prevents
ObjectDisposedExceptionfrom scoped in singleton - Explicit: Code clearly shows scope creation
- Flexibility: Can create multiple scopes per operation
- Standard Pattern: Well-known .NET pattern
- Testability: Easy to replace implementations with mocks via constructor injection
- Modularity: Each module self-contained with own service registrations
- Lifetime Safety: Factory pattern prevents captive dependencies
- Performance: Singleton caching for expensive, stateless services
- Maintainability: Services registered in predictable locations (module
Register()) - Discoverability: IDE navigation from interface to implementation
- Composition: Decorator pattern enables cross-cutting concerns
- Validation: Compile-time constructor checks ensure dependencies available
- Complexity: Understanding lifetimes requires learning curve
- Indirection: More interfaces and abstractions
- Debugging: Stack traces deeper due to decorator chains
- Boilerplate: Factory pattern adds code in singletons
- Resolution Failures: Service not registered errors only at runtime
- Module Registration: Each module calls
services.Add*()inRegister()method - Open Generics: Powerful but can be confusing for newcomers
- Constructor Injection: Preferred over property/method injection
When registering a service, ask:
- Is it stateless and thread-safe? → Singleton
- Does it depend on DbContext/request data? → Scoped
- Is it lightweight and stateless? → Transient
- Does it implement IDisposable? → Scoped or Transient (never Singleton)
- Is it injected into a singleton? → Use factory pattern if scoped
public class MyModule : WebModuleBase
{
public override IServiceCollection Register(
IServiceCollection services,
IConfiguration configuration = null,
IWebHostEnvironment environment = null)
{
// 1. Configuration
var moduleConfig = this.Configure<MyModuleConfiguration>(services, configuration);
// 2. Domain Services
services.AddScoped<IMyDomainService, MyDomainService>();
// 3. Application Services (MediatR)
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(MyModule).Assembly));
// 4. Infrastructure - Database
services.AddSqlServerDbContext<MyDbContext>(o => o
.UseConnectionString(moduleConfig.ConnectionStrings["Default"]));
// 5. Infrastructure - Repositories
services.AddEntityFrameworkRepository<MyEntity, MyDbContext>()
.WithBehavior<RepositoryLoggingBehavior<MyEntity>>();
// 6. Presentation - Endpoints
services.AddEndpoints<MyEndpoints>();
// 7. Presentation - Mapping
services.AddMapping().WithMapster<MyModuleMapperRegister>();
// 8. Jobs (if any)
services.AddJobScheduling(o => o.StartupDelay("00:00:10"), configuration)
.WithJob<MyJob>()
.Cron(CronExpressions.EveryHour)
.Named($"{this.Name}_{nameof(MyJob)}")
.RegisterScoped();
return services;
}
}// Singleton - Configuration (immutable)
services.AddSingleton<IMyConfiguration>(sp =>
{
var config = new MyConfiguration();
configuration.Bind("MyModule", config);
return config;
});
// Singleton - Mapper (stateless)
services.AddSingleton<IMapper>(sp =>
{
var config = new TypeAdapterConfig();
config.Scan(typeof(MyModule).Assembly);
return new Mapper(config);
});
// Scoped - DbContext
services.AddDbContext<MyDbContext>(ServiceLifetime.Scoped);
// Scoped - Repository (depends on DbContext)
services.AddScoped<IGenericRepository<MyEntity>, EntityFrameworkRepository<MyEntity, MyDbContext>>();
// Scoped - Request Handler
services.AddScoped<IRequestHandler<MyCommand, Result>, MyCommandHandler>();
// Transient - Validator
services.AddTransient<IValidator<MyCommand>, MyCommand.Validator>();
// Transient - Specification
services.AddTransient<ISpecification<MyEntity>, MyEntitySpecification>();// WRONG: Repository (scoped) injected into singleton
public class MySingletonService
{
private readonly IGenericRepository<Customer> repository; // SCOPED dependency
public MySingletonService(IGenericRepository<Customer> repository)
{
this.repository = repository; // Captured in singleton!
}
public async Task DoWork()
{
await this.repository.FindAllAsync(); // ObjectDisposedException after first request
}
}
// Correct: Use IServiceScopeFactory
public class MySingletonService
{
private readonly IServiceScopeFactory scopeFactory;
public MySingletonService(IServiceScopeFactory scopeFactory)
{
this.scopeFactory = scopeFactory;
}
public async Task DoWork()
{
using var scope = this.scopeFactory.CreateScope();
var repository = scope.ServiceProvider.GetRequiredService<IGenericRepository<Customer>>();
await repository.FindAllAsync();
}
}// Override services in WebApplicationFactory
builder.ConfigureServices(services =>
{
// Replace real repository with mock
services.RemoveAll<IGenericRepository<Customer>>();
services.AddScoped<IGenericRepository<Customer>>(sp =>
{
var mock = Substitute.For<IGenericRepository<Customer>>();
mock.FindAllAsync().Returns(Result<IEnumerable<Customer>>.Success(testCustomers));
return mock;
});
});// Register IGenericRepository<T> for all entity types
services.AddScoped(typeof(IGenericRepository<>), typeof(EntityFrameworkRepository<,>));
// Resolves to:
// IGenericRepository<Customer> → EntityFrameworkRepository<Customer, CoreDbContext>
// IGenericRepository<Order> → EntityFrameworkRepository<Order, OrderDbContext>
// Register IRequestHandler<TRequest, TResponse> for all commands
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CoreModule).Assembly));// Register different implementations based on environment
if (environment.IsDevelopment())
{
services.AddSingleton<IEmailSender, FakeEmailSender>();
}
else
{
services.AddSingleton<IEmailSender, SmtpEmailSender>();
}
// Register based on configuration
if (moduleConfiguration.UseCache)
{
services.AddSingleton<ICache, RedisCache>();
}
else
{
services.AddSingleton<ICache, MemoryCache>();
}var builder = new ContainerBuilder();
builder.RegisterType<CustomerRepository>().As<IGenericRepository<Customer>>().InstancePerLifetimeScope();Rejected because:
- Additional dependency (Microsoft.Extensions.DependencyInjection built-in)
- ASP.NET Core optimized for built-in container
- More features than needed for this application
- Team already familiar with MS DI
var repository = new CustomerRepository(new CoreDbContext());
var handler = new CustomerCreateCommandHandler(repository, logger);Rejected because:
- No lifetime management (manual disposal)
- Hard to test (tightly coupled)
- No decorator patterns
- Violates dependency inversion principle
var repository = ServiceLocator.GetService<IGenericRepository<Customer>>();Rejected because:
- Anti-pattern (hides dependencies)
- Harder to test (requires global state)
- No compile-time safety
- Obscures dependency graph
public class MyService
{
[Inject]
public ILogger Logger { get; set; }
}Rejected because:
- Not well-supported in Microsoft.Extensions.DependencyInjection
- Constructor injection preferred (explicit dependencies)
- Nullable properties require null checks
- Harder to enforce required dependencies
- ADR-0003: Module-based registration pattern
- ADR-0015: Factory pattern for scoped dependencies in jobs
- ADR-0017: Service replacement in tests
- ADR-0004: Decorator registration pattern
// Simple registration
services.AddScoped<IMyService, MyService>();
// Factory registration
services.AddScoped<IMyService>(sp =>
{
var logger = sp.GetRequiredService<ILogger<MyService>>();
var config = sp.GetRequiredService<IConfiguration>();
return new MyService(logger, config["MyKey"]);
});
// Implementation instance
services.AddSingleton<IMyService>(new MyService());
// Try add (only if not already registered)
services.TryAddScoped<IMyService, MyService>();
// Replace existing registration
services.Replace(ServiceDescriptor.Scoped<IMyService, NewMyService>());
// Remove registration
services.RemoveAll<IMyService>();// Enable scope validation in development
public static IHost CreateHost(string[] args)
{
return Host.CreateDefaultBuilder(args)
.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
options.ValidateOnBuild = true;
})
.Build();
}WRONG Captive Dependency:
services.AddSingleton<MySingleton>(); // Captures scoped DbContextWRONG Disposable Singleton:
services.AddSingleton<IDisposable, MyDisposable>(); // Never disposedWRONG Circular Dependency:
services.AddScoped<A>(); // Depends on B
services.AddScoped<B>(); // Depends on ACORRECT Use Lazy<T> for circular dependencies:
public class A(Lazy<B> lazyB) { }
public class B(Lazy<A> lazyA) { }- Module Registration:
src/Modules/<Module>/<Module>.Presentation/ModuleModule.cs - Service Interfaces:
src/Modules/<Module>/<Module>.Application/(abstractions) - Service Implementations: Layer-specific folders (Domain, Application, Infrastructure)