662 lines
17 KiB
Markdown
662 lines
17 KiB
Markdown
# 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<CreateUserCommand, int, CreateUserCommandHandler>();
|
|
|
|
// Behind the scenes:
|
|
builder.Services.AddScoped<ICommandHandler<CreateUserCommand, int>, CreateUserCommandHandler>();
|
|
```
|
|
|
|
### Registration with Validator
|
|
|
|
```csharp
|
|
// Command with validator
|
|
builder.Services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler, CreateUserCommandValidator>();
|
|
|
|
// Equivalent to:
|
|
builder.Services.AddScoped<ICommandHandler<CreateUserCommand, int>, CreateUserCommandHandler>();
|
|
builder.Services.AddTransient<IValidator<CreateUserCommand>, CreateUserCommandValidator>();
|
|
```
|
|
|
|
## Handler Lifetimes
|
|
|
|
### Scoped (Default and Recommended)
|
|
|
|
**Default lifetime for all handlers:**
|
|
|
|
```csharp
|
|
services.AddCommand<CreateUserCommand, int, CreateUserCommandHandler>();
|
|
// 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<CreateUserCommand, int>
|
|
{
|
|
private readonly ApplicationDbContext _context; // Scoped
|
|
|
|
public CreateUserCommandHandler(ApplicationDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
public async Task<int> 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<ICommandHandler<CreateUserCommand, int>, 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<CreateUserCommand>
|
|
{
|
|
public CreateUserCommandValidator()
|
|
{
|
|
RuleFor(x => x.Name).NotEmpty();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Singleton
|
|
|
|
**One instance for application lifetime:**
|
|
|
|
```csharp
|
|
// Manually register as Singleton
|
|
services.AddSingleton<ICommandHandler<CreateUserCommand, int>, 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<CreateUserCommand, int>
|
|
{
|
|
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<CacheUserCommand>
|
|
{
|
|
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<ApplicationDbContext>();
|
|
// Use context...
|
|
}
|
|
}
|
|
```
|
|
|
|
## Constructor Injection
|
|
|
|
### Basic Injection
|
|
|
|
```csharp
|
|
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
|
|
{
|
|
private readonly IUserRepository _userRepository;
|
|
private readonly ILogger<CreateUserCommandHandler> _logger;
|
|
|
|
public CreateUserCommandHandler(
|
|
IUserRepository userRepository,
|
|
ILogger<CreateUserCommandHandler> logger)
|
|
{
|
|
_userRepository = userRepository;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<int> 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<PlaceOrderCommand, int>
|
|
{
|
|
private readonly IOrderRepository _orders;
|
|
private readonly IInventoryService _inventory;
|
|
private readonly IPaymentService _payment;
|
|
private readonly IEmailService _email;
|
|
private readonly ILogger<PlaceOrderCommandHandler> _logger;
|
|
|
|
public PlaceOrderCommandHandler(
|
|
IOrderRepository orders,
|
|
IInventoryService inventory,
|
|
IPaymentService payment,
|
|
IEmailService email,
|
|
ILogger<PlaceOrderCommandHandler> logger)
|
|
{
|
|
_orders = orders;
|
|
_inventory = inventory;
|
|
_payment = payment;
|
|
_email = email;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<int> HandleAsync(PlaceOrderCommand command, CancellationToken cancellationToken)
|
|
{
|
|
// Use all dependencies...
|
|
}
|
|
}
|
|
```
|
|
|
|
### Optional Dependencies
|
|
|
|
```csharp
|
|
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
|
|
{
|
|
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<int> 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<User?> GetByIdAsync(int id, CancellationToken cancellationToken);
|
|
Task<int> 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<User?> GetByIdAsync(int id, CancellationToken cancellationToken)
|
|
{
|
|
return await _context.Users.FindAsync(new object[] { id }, cancellationToken);
|
|
}
|
|
|
|
public async Task<int> AddAsync(User user, CancellationToken cancellationToken)
|
|
{
|
|
_context.Users.Add(user);
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
return user.Id;
|
|
}
|
|
}
|
|
|
|
// Registration
|
|
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
|
|
|
// Usage in handler
|
|
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, int>
|
|
{
|
|
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<IEmailService, SendGridEmailService>();
|
|
```
|
|
|
|
### 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<EmailOptions>(builder.Configuration.GetSection("Email"));
|
|
|
|
// Injection
|
|
public class SendGridEmailService : IEmailService
|
|
{
|
|
private readonly EmailOptions _options;
|
|
|
|
public SendGridEmailService(IOptions<EmailOptions> options)
|
|
{
|
|
_options = options.Value;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Decorator Pattern
|
|
|
|
```csharp
|
|
// Base interface
|
|
public interface ICommandHandler<in TCommand, TResult>
|
|
{
|
|
Task<TResult> HandleAsync(TCommand command, CancellationToken cancellationToken);
|
|
}
|
|
|
|
// Decorator for logging
|
|
public class LoggingCommandHandlerDecorator<TCommand, TResult> : ICommandHandler<TCommand, TResult>
|
|
{
|
|
private readonly ICommandHandler<TCommand, TResult> _inner;
|
|
private readonly ILogger _logger;
|
|
|
|
public LoggingCommandHandlerDecorator(
|
|
ICommandHandler<TCommand, TResult> inner,
|
|
ILogger<LoggingCommandHandlerDecorator<TCommand, TResult>> logger)
|
|
{
|
|
_inner = inner;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<TResult> 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<CreateUserCommand, int, CreateUserCommandHandler, CreateUserCommandValidator>();
|
|
services.AddCommand<UpdateUserCommand, UpdateUserCommandHandler, UpdateUserCommandValidator>();
|
|
services.AddCommand<DeleteUserCommand, DeleteUserCommandHandler>();
|
|
|
|
// Queries
|
|
services.AddQuery<GetUserQuery, UserDto, GetUserQueryHandler>();
|
|
services.AddQuery<ListUsersQuery, List<UserDto>, ListUsersQueryHandler>();
|
|
|
|
// Repositories
|
|
services.AddScoped<IUserRepository, UserRepository>();
|
|
|
|
return services;
|
|
}
|
|
|
|
public static IServiceCollection AddOrderFeatures(this IServiceCollection services)
|
|
{
|
|
// Commands
|
|
services.AddCommand<PlaceOrderCommand, int, PlaceOrderCommandHandler>();
|
|
services.AddCommand<CancelOrderCommand, CancelOrderCommandHandler>();
|
|
|
|
// Queries
|
|
services.AddQuery<GetOrderQuery, OrderDto, GetOrderQueryHandler>();
|
|
|
|
// Repositories
|
|
services.AddScoped<IOrderRepository, OrderRepository>();
|
|
|
|
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<CreateUserCommand, int, CreateUserCommandHandler>();
|
|
services.AddQuery<GetUserQuery, UserDto, GetUserQueryHandler>();
|
|
services.AddScoped<IUserRepository, UserRepository>();
|
|
}
|
|
}
|
|
|
|
// Auto-register all modules
|
|
var modules = typeof(Program).Assembly
|
|
.GetTypes()
|
|
.Where(t => typeof(IModule).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract)
|
|
.Select(Activator.CreateInstance)
|
|
.Cast<IModule>();
|
|
|
|
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<IUserRepository>();
|
|
mockRepository
|
|
.Setup(r => r.AddAsync(It.IsAny<User>(), It.IsAny<CancellationToken>()))
|
|
.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<User>(), It.IsAny<CancellationToken>()), Times.Once);
|
|
}
|
|
```
|
|
|
|
### Integration Testing
|
|
|
|
Use WebApplicationFactory:
|
|
|
|
```csharp
|
|
public class ApiTests : IClassFixture<WebApplicationFactory<Program>>
|
|
{
|
|
private readonly WebApplicationFactory<Program> _factory;
|
|
|
|
public ApiTests(WebApplicationFactory<Program> 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<int>();
|
|
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<PlaceOrderCommand, int>
|
|
{
|
|
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
|