Scope Validation
Scope validation helps catch lifetime mismatches and missing dependencies at build time, preventing runtime errors.
Overview
The IoC container provides two validation options:
validateScopes- Validates lifetime mismatches (e.g., scoped service injected into singleton)validateOnBuild- Validates all dependencies exist at build time
Both options default to false. Enable them explicitly for validation.
Comparison with .NET Core
In .NET Core, these options work similarly:
// .NET Core - ServiceProviderOptions
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true; // Default: false
options.ValidateOnBuild = true; // Default: false (available in .NET 6+)
});Default Behavior in .NET Core:
ValidateScopes:false(default) - No validation, allows problematic lifetime combinationsValidateOnBuild:false(default) - No build-time validation, errors appear at runtime
Best Practice in .NET Core:
- Enable
ValidateScopesandValidateOnBuildin development environment - Disable them in production for better performance
- This is exactly the same approach you should use with this library!
// .NET Core - Typical usage pattern
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = builder.Environment.IsDevelopment();
options.ValidateOnBuild = builder.Environment.IsDevelopment();
});Our Implementation: Our library follows the same pattern as .NET Core - both options default to false and work identically. The validation logic and error messages are also similar to .NET Core's implementation.
validateScopes Option
validateScopes: false (default)
No validation, allows any lifetime combination:
// ⚠️ WARNING: This works but can cause issues!
services.addScoped<ILogger>(ILoggerToken, Logger);
services.addSingleton<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);
const provider = services.buildServiceProvider({ validateScopes: false });
// ✅ Provider created successfully, but this is problematic!
// Scoped service injected into singleton can cause issues in production
const userService = await provider.getRequiredService<IUserService>(IUserServiceToken);
// ⚠️ Works, but the scoped logger instance will be shared across all singleton instances
// This breaks the scoped lifetime guarantee!validateScopes: true
Validates lifetime mismatches at runtime:
// ❌ ERROR: Scoped service injected into singleton
services.addScoped<ILogger>(ILoggerToken, Logger);
services.addSingleton<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);
const provider = services.buildServiceProvider({ validateScopes: true });
// ❌ Throws immediately: "Cannot inject scoped service 'ILogger' into singleton service 'IUserService'"
// This prevents the problematic configuration!
// ✅ CORRECT: Scoped service used within scope
services.addSingleton<ILogger>(ILoggerToken, Logger);
services.addScoped<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);
const provider = services.buildServiceProvider({ validateScopes: true });
const scope = provider.createScope();
const userService = await scope.getRequiredService<IUserService>(IUserServiceToken); // ✅ Works!
// Scoped service is correctly resolved from a scope, not from root providerAdditional validateScopes Examples
Example 1: Root provider cannot resolve scoped services when validation is enabled
services.addScoped<IUserService>(IUserServiceToken, UserService);
const provider = services.buildServiceProvider({ validateScopes: true });
// ❌ ERROR: Cannot resolve scoped service from root provider
await provider.getRequiredService<IUserService>(IUserServiceToken);
// Throws: "Cannot resolve scoped service 'IUserService' from root provider. Create a scope first."
// ✅ CORRECT: Resolve from scope
const scope = provider.createScope();
await scope.getRequiredService<IUserService>(IUserServiceToken); // ✅ Works!Example 2: Factory with scoped dependency
services.addScoped<ILogger>(ILoggerToken, Logger);
services.addSingleton<IUserService>(IUserServiceToken, (provider) => {
const logger = provider.getRequiredService<ILogger>(ILoggerToken); // ❌ ERROR with validateScopes: true
return new UserService(logger);
});
const provider = services.buildServiceProvider({ validateScopes: true });
// ❌ Throws when factory tries to resolve scoped service from root providervalidateOnBuild Option
validateOnBuild: false (default)
No validation at build time, errors appear at runtime:
// ⚠️ Missing dependency - not detected until runtime!
services.addScoped<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);
// ILoggerToken is not registered!
const provider = services.buildServiceProvider({ validateOnBuild: false });
// ✅ Provider created successfully, but the error will occur later
// Later, when trying to use the service:
try {
const scope = provider.createScope();
await scope.getRequiredService<IUserService>(IUserServiceToken);
// ❌ Throws: "No provider found for token: Symbol(ILogger)"
// Error discovered at runtime, not at build time!
} catch (error) {
console.error('Runtime error:', error);
}validateOnBuild: true
Validates all dependencies at build time (catches missing dependencies early):
// ❌ ERROR: Missing dependency detected at build time
services.addScoped<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);
// ILoggerToken is not registered!
try {
const provider = services.buildServiceProvider({ validateOnBuild: true });
// ❌ Throws immediately: "Validation failed on build: Missing dependency: ILogger required by IUserService"
// Error caught at build time, before any services are used!
} catch (error) {
console.error('Build-time validation error:', error);
// You can fix the issue before deploying to production
}
// ✅ CORRECT: All dependencies registered
services.addSingleton<ILogger>(ILoggerToken, Logger);
services.addScoped<IUserService>(IUserServiceToken, UserService, [ILoggerToken]);
const provider = services.buildServiceProvider({ validateOnBuild: true }); // ✅ No errors!
// All dependencies are validated and confirmed to existAdditional validateOnBuild Examples
Example 1: Multiple missing dependencies - all are reported
services.addSingleton<IServiceA>(IServiceAToken, ServiceA, [IServiceBToken, IServiceCToken]);
// Both IServiceBToken and IServiceCToken are missing!
const provider = services.buildServiceProvider({ validateOnBuild: true });
// ❌ Throws: "Validation failed on build: Missing dependency: IServiceB required by IServiceA"
// All missing dependencies are reported in the error messageExample 2: Deep dependency chain validation
services.addSingleton<IServiceA>(IServiceAToken, ServiceA, [IServiceBToken]);
services.addSingleton<IServiceB>(IServiceBToken, ServiceB, [IServiceCToken]);
// IServiceCToken is missing!
const provider = services.buildServiceProvider({ validateOnBuild: true });
// ❌ Throws: "Validation failed on build: Missing dependency: IServiceC required by IServiceB"
// Validates the entire dependency chain recursively, not just direct dependenciesUsing Both Options Together
Best practice: Enable both validations in development:
// Best practice: Enable both validations in development
const provider = services.buildServiceProvider({
validateScopes: true, // Catch lifetime mismatches
validateOnBuild: true, // Catch missing dependencies early
});
// This ensures:
// 1. All dependencies exist (validateOnBuild)
// 2. Lifetime rules are followed (validateScopes)
// 3. Errors are caught before production deploymentWhen to Enable
Development
Always enable in development to catch issues early:
const isDevelopment = process.env.NODE_ENV === 'development';
const provider = services.buildServiceProvider({
validateScopes: isDevelopment,
validateOnBuild: isDevelopment,
});Production
You can disable in production for better performance, but it's recommended to keep them enabled to prevent runtime errors.
Error Messages
Validation provides clear error messages:
Scope Validation Errors:
Cannot resolve scoped service 'ILogger' from root provider. Create a scope first.Cannot inject scoped service 'ILogger' into singleton service 'IUserService'.Build Validation Errors:
Validation failed on build: Missing dependency: ILogger required by IUserServiceBest Practices
- Enable in development - Catch lifetime and dependency issues early
- Keep enabled in production - Prevents runtime errors (optional, for performance you can disable)
- Read error messages carefully - They tell you exactly what's wrong
- Fix the root cause - Don't disable validation to work around issues