# Dependency Injection Understanding DI patterns, handler lifetime management, and service registration in Svrnty.CQRS. ## Overview Svrnty.CQRS leverages ASP.NET Core's built-in dependency injection container for handler registration and resolution. Understanding handler lifetimes and DI patterns is crucial for building maintainable applications. ## Handler Registration ### Basic Registration Handlers are registered with specific lifetimes: ```csharp // Command handler (default: Scoped) builder.Services.AddCommand(); // Behind the scenes: builder.Services.AddScoped, CreateUserCommandHandler>(); ``` ### Registration with Validator ```csharp // Command with validator builder.Services.AddCommand(); // Equivalent to: builder.Services.AddScoped, CreateUserCommandHandler>(); builder.Services.AddTransient, CreateUserCommandValidator>(); ``` ## Handler Lifetimes ### Scoped (Default and Recommended) **Default lifetime for all handlers:** ```csharp services.AddCommand(); // Handler is Scoped ``` **Characteristics:** - One instance per HTTP request - Disposed at end of request - Can inject DbContext, scoped services - **Recommended** for most handlers **When to use:** - ✅ Handlers that use EF Core DbContext - ✅ Handlers that use scoped services - ✅ Default choice (95% of cases) **Example:** ```csharp public class CreateUserCommandHandler : ICommandHandler { private readonly ApplicationDbContext _context; // Scoped public CreateUserCommandHandler(ApplicationDbContext context) { _context = context; } public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { var user = new User { Name = command.Name }; _context.Users.Add(user); await _context.SaveChangesAsync(cancellationToken); return user.Id; } } ``` ### Transient **One instance per injection:** ```csharp // Manually register as Transient services.AddTransient, CreateUserCommandHandler>(); ``` **Characteristics:** - New instance each time it's injected - Disposed after use - Lightweight, no state **When to use:** - ✅ Stateless handlers - ✅ Lightweight operations - ✅ Validators (default) **Example:** ```csharp // Validators are Transient by default public class CreateUserCommandValidator : AbstractValidator { public CreateUserCommandValidator() { RuleFor(x => x.Name).NotEmpty(); } } ``` ### Singleton **One instance for application lifetime:** ```csharp // Manually register as Singleton services.AddSingleton, CreateUserCommandHandler>(); ``` **Characteristics:** - Single instance shared across all requests - Never disposed (until app shutdown) - Must be thread-safe - Cannot inject scoped services **When to use:** - ⚠️ Rarely used for handlers - ⚠️ Only for stateless, thread-safe handlers - ❌ Not recommended (can't inject DbContext) **Example:** ```csharp // ❌ Bad: Singleton handler with scoped dependency public class CreateUserCommandHandler : ICommandHandler { private readonly ApplicationDbContext _context; // ERROR: Cannot inject scoped service // This will throw an exception at runtime! } // ✅ Good: Singleton handler with singleton dependencies public class CacheUserCommandHandler : ICommandHandler { private readonly IMemoryCache _cache; // Singleton - OK public CacheUserCommandHandler(IMemoryCache cache) { _cache = cache; } } ``` ## Service Lifetime Rules ### Rule 1: Service Can Inject Same or Longer Lifetime ``` Singleton can inject: ✅ Singleton ❌ Scoped ❌ Transient Scoped can inject: ✅ Singleton ✅ Scoped ✅ Transient Transient can inject: ✅ Singleton ✅ Scoped ✅ Transient ``` ### Rule 2: Don't Capture Shorter Lifetimes ```csharp // ❌ Bad: Singleton captures scoped public class SingletonService { private readonly ApplicationDbContext _context; // Scoped // ERROR: Captive dependency } // ✅ Good: Use IServiceProvider for scoped resolution public class SingletonService { private readonly IServiceProvider _serviceProvider; public void DoWork() { using var scope = _serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); // Use context... } } ``` ## Constructor Injection ### Basic Injection ```csharp public class CreateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; private readonly ILogger _logger; public CreateUserCommandHandler( IUserRepository userRepository, ILogger logger) { _userRepository = userRepository; _logger = logger; } public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { _logger.LogInformation("Creating user: {Email}", command.Email); return await _userRepository.AddAsync(new User { Name = command.Name }); } } ``` ### Multiple Dependencies ```csharp public class PlaceOrderCommandHandler : ICommandHandler { private readonly IOrderRepository _orders; private readonly IInventoryService _inventory; private readonly IPaymentService _payment; private readonly IEmailService _email; private readonly ILogger _logger; public PlaceOrderCommandHandler( IOrderRepository orders, IInventoryService inventory, IPaymentService payment, IEmailService email, ILogger logger) { _orders = orders; _inventory = inventory; _payment = payment; _email = email; _logger = logger; } public async Task HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken) { // Use all dependencies... } } ``` ### Optional Dependencies ```csharp public class CreateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; private readonly IEmailService? _emailService; // Optional public CreateUserCommandHandler( IUserRepository userRepository, IEmailService? emailService = null) // Optional parameter { _userRepository = userRepository; _emailService = emailService; } public async Task HandleAsync(CreateUserCommand command, CancellationToken cancellationToken) { var userId = await _userRepository.AddAsync(new User { Name = command.Name }); // Send email if service available if (_emailService != null) { await _emailService.SendWelcomeEmailAsync(command.Email); } return userId; } } ``` ## Common DI Patterns ### Repository Pattern ```csharp // Interface in Domain layer public interface IUserRepository { Task GetByIdAsync(int id, CancellationToken cancellationToken); Task AddAsync(User user, CancellationToken cancellationToken); } // Implementation in Infrastructure layer public class UserRepository : IUserRepository { private readonly ApplicationDbContext _context; public UserRepository(ApplicationDbContext context) { _context = context; } public async Task GetByIdAsync(int id, CancellationToken cancellationToken) { return await _context.Users.FindAsync(new object[] { id }, cancellationToken); } public async Task AddAsync(User user, CancellationToken cancellationToken) { _context.Users.Add(user); await _context.SaveChangesAsync(cancellationToken); return user.Id; } } // Registration builder.Services.AddScoped(); // Usage in handler public class CreateUserCommandHandler : ICommandHandler { private readonly IUserRepository _userRepository; public CreateUserCommandHandler(IUserRepository userRepository) { _userRepository = userRepository; } } ``` ### Service Pattern ```csharp // Service interface public interface IEmailService { Task SendWelcomeEmailAsync(string email, CancellationToken cancellationToken = default); } // Implementation public class SendGridEmailService : IEmailService { private readonly IConfiguration _configuration; public SendGridEmailService(IConfiguration configuration) { _configuration = configuration; } public async Task SendWelcomeEmailAsync(string email, CancellationToken cancellationToken) { // Send via SendGrid... } } // Registration builder.Services.AddScoped(); ``` ### Options Pattern ```csharp // Options class public class EmailOptions { public string ApiKey { get; set; } = string.Empty; public string FromEmail { get; set; } = string.Empty; } // appsettings.json { "Email": { "ApiKey": "your-api-key", "FromEmail": "noreply@example.com" } } // Registration builder.Services.Configure(builder.Configuration.GetSection("Email")); // Injection public class SendGridEmailService : IEmailService { private readonly EmailOptions _options; public SendGridEmailService(IOptions options) { _options = options.Value; } } ``` ### Decorator Pattern ```csharp // Base interface public interface ICommandHandler { Task HandleAsync(TCommand command, CancellationToken cancellationToken); } // Decorator for logging public class LoggingCommandHandlerDecorator : ICommandHandler { private readonly ICommandHandler _inner; private readonly ILogger _logger; public LoggingCommandHandlerDecorator( ICommandHandler inner, ILogger> logger) { _inner = inner; _logger = logger; } public async Task HandleAsync(TCommand command, CancellationToken cancellationToken) { _logger.LogInformation("Executing command: {CommandType}", typeof(TCommand).Name); var result = await _inner.HandleAsync(command, cancellationToken); _logger.LogInformation("Command executed successfully"); return result; } } // Registration with Scrutor builder.Services.Decorate(typeof(ICommandHandler<,>), typeof(LoggingCommandHandlerDecorator<,>)); ``` ## Registration Organization ### Extension Methods Group related registrations: ```csharp // Extensions/ServiceCollectionExtensions.cs public static class ServiceCollectionExtensions { public static IServiceCollection AddUserFeatures(this IServiceCollection services) { // Commands services.AddCommand(); services.AddCommand(); services.AddCommand(); // Queries services.AddQuery(); services.AddQuery, ListUsersQueryHandler>(); // Repositories services.AddScoped(); return services; } public static IServiceCollection AddOrderFeatures(this IServiceCollection services) { // Commands services.AddCommand(); services.AddCommand(); // Queries services.AddQuery(); // Repositories services.AddScoped(); return services; } } // Usage in Program.cs builder.Services.AddUserFeatures(); builder.Services.AddOrderFeatures(); ``` ### Module Pattern ```csharp public interface IModule { void RegisterServices(IServiceCollection services, IConfiguration configuration); } public class UserModule : IModule { public void RegisterServices(IServiceCollection services, IConfiguration configuration) { services.AddCommand(); services.AddQuery(); services.AddScoped(); } } // Auto-register all modules var modules = typeof(Program).Assembly .GetTypes() .Where(t => typeof(IModule).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract) .Select(Activator.CreateInstance) .Cast(); foreach (var module in modules) { module.RegisterServices(builder.Services, builder.Configuration); } ``` ## Testing with DI ### Unit Testing Mock dependencies: ```csharp [Fact] public async Task CreateUser_WithValidData_ReturnsUserId() { // Arrange var mockRepository = new Mock(); mockRepository .Setup(r => r.AddAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(123); var handler = new CreateUserCommandHandler(mockRepository.Object); var command = new CreateUserCommand { Name = "Alice", Email = "alice@example.com" }; // Act var result = await handler.HandleAsync(command, CancellationToken.None); // Assert Assert.Equal(123, result); mockRepository.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Once); } ``` ### Integration Testing Use WebApplicationFactory: ```csharp public class ApiTests : IClassFixture> { private readonly WebApplicationFactory _factory; public ApiTests(WebApplicationFactory factory) { _factory = factory; } [Fact] public async Task CreateUser_WithValidData_Returns201() { // Arrange var client = _factory.CreateClient(); var command = new { name = "Alice", email = "alice@example.com" }; // Act var response = await client.PostAsJsonAsync("/api/command/createUser", command); // Assert response.EnsureSuccessStatusCode(); var userId = await response.Content.ReadFromJsonAsync(); Assert.True(userId > 0); } } ``` ## Best Practices ### ✅ DO - Use Scoped lifetime for handlers (default) - Inject interfaces, not concrete types - Keep constructors simple (assignment only) - Use readonly fields for dependencies - Group registrations in extension methods - Use Options pattern for configuration - Test handlers in isolation ### ❌ DON'T - Don't use Singleton for handlers (can't inject DbContext) - Don't inject IServiceProvider into handlers (service locator anti-pattern) - Don't perform logic in constructors - Don't create circular dependencies - Don't inject too many dependencies (5+ is a code smell) - Don't mix lifetimes incorrectly (singleton injecting scoped) ## Common Issues ### Issue 1: Captive Dependency **Problem:** Singleton captures scoped dependency ```csharp // ❌ Bad public class SingletonHandler { private readonly ApplicationDbContext _context; // Scoped - ERROR! } ``` **Solution:** Use correct lifetime ```csharp // ✅ Good [ServiceLifetime(ServiceLifetime.Scoped)] public class ScopedHandler { private readonly ApplicationDbContext _context; // OK now } ``` ### Issue 2: Disposed DbContext **Problem:** DbContext disposed before use **Cause:** Improper lifetime management **Solution:** Ensure handler is Scoped and DbContext is Scoped ### Issue 3: Too Many Dependencies **Problem:** Handler with 10+ constructor parameters **Solution:** Refactor into domain services ```csharp // ✅ Better: Extract domain service public class OrderService { // Multiple dependencies here } public class PlaceOrderCommandHandler : ICommandHandler { private readonly OrderService _orderService; // Single dependency } ``` ## What's Next? - **[Extensibility Points](extensibility-points.md)** - Framework customization - **[Best Practices: Testing](../best-practices/testing.md)** - Testing strategies ## See Also - [Modular Solution Structure](modular-solution-structure.md) - Organizing projects - [Microsoft: Dependency Injection](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) - Official DI docs - [Best Practices](../best-practices/README.md) - Production-ready patterns