| name | dependency-injection-review |
| description | Review code for proper DI patterns using DryIoc. Ensures no static singletons, validates constructor injection and service lifetimes. Use when reviewing code, refactoring static access, or debugging DI issues. |
Dependency Injection Review
Table of Contents
- Overview
- When to Use
- Quick Reference
- Core Principles
- Review Checklist
- Registration Patterns
- Step-by-Step DI Review Workflow
- Debugging DI Issues
Overview
This skill audits code for adherence to the game engine's dependency injection architecture using DryIoc. It ensures all services use constructor injection, identifies static singleton violations, and validates service registration patterns.
When to Use
Invoke this skill when:
- Reviewing new code for DI compliance
- Refactoring static singletons to use DI
- Debugging service resolution errors
- Adding new services to the container
- Questions about service lifetime and registration
- Investigating circular dependency issues
Quick Reference
✅ Do
- Primary constructors for all classes with dependencies
- Register services in
Program.cs - Use events for decoupling
- Interface-based design
- Use Singleton for stateful services
❌ Don't
- Static singletons
- Manual
new()for services - Null validation in constructors (non-nullable types handle this)
- Circular dependencies
- Concrete dependencies everywhere
- Use Transient for managers/factories
Core Principles
The Golden Rule
NEVER create static singletons! All singleton instances must be registered in the DI container.
Exceptions
The ONLY acceptable static classes are pure constant classes:
EditorUIConstants- UI sizing and styling constantsRenderingConstants- Rendering configuration constants
Everything else uses dependency injection.
Review Checklist
1. Constructor Injection Pattern
All dependencies must be injected through the primary constructor.
✅ CORRECT (Use Primary Constructor):
public class AnimationSystem(
ITextureFactory textureFactory,
IResourceManager resourceManager) : ISystem
{
// Dependencies are automatically available as private readonly fields
// Use textureFactory and resourceManager directly in methods
}
❌ FORBIDDEN - Static Singleton:
public class AnimationSystem
{
private static AnimationSystem? _instance;
public static AnimationSystem Instance => _instance ??= new AnimationSystem();
private AnimationSystem() { } // Private constructor
}
❌ FORBIDDEN - Property Injection:
public class AnimationSystem
{
public ITextureFactory TextureFactory { get; set; } // Don't use property injection!
public AnimationSystem() { }
}
❌ FORBIDDEN - Service Locator Pattern:
public class AnimationSystem
{
private readonly ITextureFactory _textureFactory;
public AnimationSystem()
{
// Don't resolve from container directly!
_textureFactory = ServiceLocator.Resolve<ITextureFactory>();
}
}
2. Service Registration
Location: Editor/Program.cs or Runtime/Program.cs
Service Lifetime Guidelines:
- Singleton (default): SceneManager, TextureFactory, ConsolePanel, RenderingSystem, ProjectManager
- Transient: ValidationService, TemporaryOperationContext, per-request processors
- Scoped: Not used in this engine (no HTTP request scope)
Example: Editor/Program.cs Registration:
// Core managers (Singleton)
container.Register<ISceneManager, SceneManager>(Reuse.Singleton);
container.Register<IProjectManager, ProjectManager>(Reuse.Singleton);
container.Register<ISelectionManager, SelectionManager>(Reuse.Singleton);
// Factories (Singleton)
container.Register<ITextureFactory, TextureFactory>(Reuse.Singleton);
container.Register<IShaderFactory, ShaderFactory>(Reuse.Singleton);
// Panels (Singleton)
container.Register<ContentBrowserPanel>(Reuse.Singleton);
container.Register<ConsolePanel>(Reuse.Singleton);
container.Register<PropertiesPanel>(Reuse.Singleton);
// Systems (Singleton)
container.Register<RenderingSystem>(Reuse.Singleton);
container.Register<AnimationSystem>(Reuse.Singleton);
// Transient services
container.Register<IValidationService, ValidationService>(Reuse.Transient);
3. Interface-Based Design
Use interfaces for services that need abstraction, testability, or multiple implementations.
✅ USE INTERFACES FOR:
// Managers - multiple implementations or testability
public interface ISceneManager
{
Scene? ActiveScene { get; }
void LoadScene(string path);
}
public class SceneManager(IDependency dep) : ISceneManager { }
// Factories - abstraction from creation logic
public interface ITextureFactory
{
Texture2D CreateTexture(string path);
}
public class TextureFactory(ICache cache) : ITextureFactory { }
// Cross-cutting concerns - different implementations per platform
public interface IRendererAPI
{
void DrawIndexed(uint indexCount);
}
public class OpenGLRendererAPI(IContext context) : IRendererAPI { }
✅ SKIP INTERFACES FOR:
// Editor panels - concrete UI implementations
public class ConsolePanel(ILogger logger) { }
// ECS Systems - concrete game logic
public class AnimationSystem(ITextureFactory factory) : ISystem { }
// Pure data classes - no behavior to abstract
public class Transform
{
public Vector3 Position { get; set; }
public Vector3 Rotation { get; set; }
}
// Component Editors - concrete UI for specific components
public class TransformComponentEditor(IFieldEditor<Vector3> vector3Editor) { }
Decision Guide:
- Will this have multiple implementations? → Use interface
- Do you need to mock it for testing? → Use interface
- Does it cross module boundaries? → Use interface
- Is it just UI or concrete game logic? → Skip interface (register concrete class)
4. Circular Dependency Detection
❌ FORBIDDEN:
// Service A depends on Service B
public class ServiceA
{
public ServiceA(IServiceB serviceB) { }
}
// Service B depends on Service A - CIRCULAR!
public class ServiceB
{
public ServiceB(IServiceA serviceA) { }
}
✅ SOLUTIONS:
Option 1: Extract shared dependency
public class ServiceA
{
public ServiceA(ISharedService shared) { }
}
public class ServiceB
{
public ServiceB(ISharedService shared) { }
}
Option 2: Use events for decoupling
public class ServiceA
{
public event Action<Data>? OnDataChanged;
}
public class ServiceB(IServiceA serviceA)
{
// Subscribe to events in constructor body or init method
public void Initialize()
{
serviceA.OnDataChanged += HandleDataChanged;
}
}
Option 3: Pass data directly
// Instead of injecting the whole service, pass only the data needed
public class ServiceA
{
public Data GetData() => _data;
}
public class ServiceB
{
public void ProcessData(Data data) // Method parameter, not constructor
{
// Process data without depending on ServiceA
}
}
Decision Tree - Choosing a Solution:
- Can you extract shared logic? → Use Option 1 (Extract shared dependency)
- Is this an observer pattern scenario? → Use Option 2 (Events)
- Does one service only need data, not behavior? → Use Option 3 (Pass data directly)
- Still circular? → Rethink your design - you may have incorrect separation of concerns
Registration Patterns
Registering with Dependencies
// Service with dependencies (using primary constructor)
public class AnimationSystem(ITextureFactory textureFactory) : ISystem
{
// Use textureFactory in methods
}
// Simple registration - DryIoc auto-resolves dependencies
container.Register<AnimationSystem>(Reuse.Singleton);
Registering with Setup
// Service needing initialization
container.Register<ISceneManager, SceneManager>(
Reuse.Singleton,
setup: Setup.With(allowDisposableTransient: true));
Step-by-Step DI Review Workflow
When reviewing code for DI compliance, follow this systematic approach:
Scan for static singletons:
grep -r "static.*Instance" --include="*.cs" Engine/ Editor/ | grep -v "Constants.cs"Check constructor injection:
- Verify all dependencies are in constructor parameters
- Ensure no property injection or service locator usage
Validate registrations:
- Open
Editor/Program.csorRuntime/Program.cs - Ensure all injected types are registered
- Verify appropriate lifetime (Singleton vs Transient)
- Open
Verify service lifetimes:
- Singleton services should NOT depend on Transient services
- Check for proper disposal of IDisposable services
Test resolution:
- Build and run the application
- Watch for DI-related errors at startup
- Circular dependencies will fail immediately
Debugging DI Issues
For detailed troubleshooting steps, common error solutions, and automated validation scripts with expected outputs, see the Debugging Guide.
Quick validation commands:
- Detect static singletons:
grep -rn "static.*Instance.*=>" --include="*.cs" Engine/ Editor/ | grep -v "Constants.cs" - Find service locator usage:
grep -rn "ServiceLocator\|\.Resolve<" --include="*.cs" Engine/ Editor/ - Check property injection:
grep -rn "{ get; set; }.*Factory\|{ get; set; }.*Manager" --include="*.cs" Engine/ Editor/