Skip to content
Back to skills

Tools Unity Vcontainer

ASecurity

VContainer dependency injection patterns for Unity including lifecycle management, scoped registrations, and common pitfalls.

  • 17 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 27, 2026
data-aigotestingdebugginggit

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add IdoCohen560/claude-unity-game-studio --skill tools-unity-vcontainer --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tools Unity Vcontainer?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tools Unity Vcontainer
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/idocohen560-tools-unity-vcontainer/badge)](https://www.skillsdirectory.com/skills/idocohen560-tools-unity-vcontainer)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: tools-unity-vcontainer
description: VContainer dependency injection patterns for Unity including lifecycle management, scoped registrations, and common pitfalls.
---

# VContainer Dependency Injection

## Overview

VContainer is a lightweight, fast DI container for Unity. This skill covers registration patterns, lifecycle management, and common pitfalls that cause crashes.

## When to Use

- Setting up dependency injection in Unity
- Creating scoped containers for features/states
- Debugging VContainerException crashes
- Writing testable Unity code
- Managing service lifetimes

## Installation

```csharp
// Package Manager
// Add to manifest.json:
"jp.hadashikick.vcontainer": "https://github.com/hadashiA/VContainer.git?path=VContainer/Assets/VContainer#1.15.4"
```

## Core Concepts

### Lifetime Types

| Lifetime | Description | Use Case |
|----------|-------------|----------|
| `Singleton` | One instance per container | Global services, managers |
| `Scoped` | One instance per scope | Feature-specific services |
| `Transient` | New instance every resolve | Factories, short-lived objects |

**Critical:** `Singleton` in a child scope is actually **scoped to that child**, not truly global!

## Registration Patterns

### Basic Registration

```csharp
public class GameInstaller : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // Interface to implementation
        builder.Register<IPlayerService, PlayerService>(Lifetime.Singleton);
        
        // Concrete type
        builder.Register<GameManager>(Lifetime.Singleton);
        
        // With interfaces
        builder.Register<AudioSystem>(Lifetime.Singleton)
            .AsImplementedInterfaces()
            .AsSelf();
    }
}
```

### Instance Registration

```csharp
protected override void Configure(IContainerBuilder builder)
{
    // Pre-existing instance
    builder.RegisterInstance(existingService);
    
    // ScriptableObject from Resources
    var config = Resources.Load<GameConfig>("GameConfig");
    builder.RegisterInstance(config);
    
    // Component on same GameObject
    builder.RegisterComponent(GetComponent<AudioSource>());
}
```

### Factory Registration

```csharp
protected override void Configure(IContainerBuilder builder)
{
    // Simple factory
    builder.RegisterFactory<Enemy>(() => new Enemy(), Lifetime.Scoped);
    
    // Factory with parameters
    builder.RegisterFactory<string, IWeapon>(name => 
        new Weapon(name), Lifetime.Transient);
    
    // Async factory
    builder.Register<IEnemyFactory, EnemyFactory>(Lifetime.Singleton);
}

public class EnemyFactory : IEnemyFactory
{
    private readonly IObjectResolver _resolver;
    
    public EnemyFactory(IObjectResolver resolver)
    {
        _resolver = resolver;
    }
    
    public Enemy Create(EnemyConfig config)
    {
        var enemy = new Enemy(config);
        _resolver.Inject(enemy);
        return enemy;
    }
}
```

### EntryPoint Registration

```csharp
protected override void Configure(IContainerBuilder builder)
{
    // IStartable - called once after container built
    builder.RegisterEntryPoint<GameInitializer>();
    
    // ITickable - called every frame
    builder.RegisterEntryPoint<GameLoop>();
    
    // IAsyncStartable - async initialization
    builder.RegisterEntryPoint<AsyncGameInitializer>();
    
    // Multiple interfaces
    builder.Register<GameManager>(Lifetime.Singleton)
        .AsImplementedInterfaces();
}

public class GameInitializer : IStartable
{
    public void Start()
    {
        // Called after container is built
    }
}

public class AsyncGameInitializer : IAsyncStartable
{
    public async UniTask StartAsync(CancellationToken ct)
    {
        await LoadGameDataAsync(ct);
    }
}
```

## Injection Patterns

### Constructor Injection (Preferred)

```csharp
public class PlayerController
{
    private readonly IInputService _input;
    private readonly IPlayerService _player;
    
    // Dependencies injected via constructor
    public PlayerController(IInputService input, IPlayerService player)
    {
        _input = input;
        _player = player;
    }
}
```

### Method Injection

```csharp
public class EnemyBehavior : MonoBehaviour
{
    private ITargetingService _targeting;
    
    [Inject]
    public void Construct(ITargetingService targeting)
    {
        _targeting = targeting;
    }
}
```

### Property Injection (Use Sparingly)

```csharp
public class DebugManager
{
    // Optional dependency
    [Inject]
    public IAnalyticsService Analytics { get; set; }
}
```

### Field Injection (Avoid in Production)

```csharp
public class LegacyComponent : MonoBehaviour
{
    [Inject] private IService _service; // Avoid - harder to test
}
```

## Scoped Containers (Child Scopes)

### Creating Child Scopes

```csharp
public class StateNavigationController
{
    private readonly LifetimeScope _rootScope;
    private readonly List<LifetimeScope> _stateStack = new();
    
    public async UniTask NavigateTo(IStateTransitionData data)
    {
        // Create child scope for new state
        var stateScope = _rootScope.CreateChild(builder =>
        {
            builder.Register<IState, WorldState>(Lifetime.Singleton);
            builder.Register<WorldController>(Lifetime.Singleton);
        });
        
        _stateStack.Add(stateScope);
        
        var state = stateScope.Container.Resolve<IState>();
        await state.Enter(data);
    }
    
    public async UniTask ExitState()
    {
        var scope = _stateStack[^1];
        _stateStack.RemoveAt(_stateStack.Count - 1);
        
        var state = scope.Container.Resolve<IState>();
        await state.Exit();
        
        // CRITICAL: Dispose scope AFTER exit completes
        await UniTask.Yield(); // Allow pending callbacks
        scope.Dispose();
    }
}
```

### Using IInstaller

```csharp
public class WorldStateInstaller : IInstaller
{
    public void Install(IContainerBuilder builder)
    {
        builder.Register<IState, WorldState>(Lifetime.Singleton);
        builder.Register<WorldController>(Lifetime.Singleton);
        builder.Register<WorldView>(Lifetime.Singleton);
    }
}

// Usage
var scope = parentScope.CreateChild(new WorldStateInstaller());
```

### Enqueue Pattern for Segment Loading

```csharp
using (LifetimeScope.Enqueue(segmentCache))
{
    var stateScope = _rootScope.CreateChild(installer);
    // segmentCache registrations included
}
```

## Common Crash Patterns & Fixes

### Problem 1: Resolving After Disposal

```csharp
// BAD: Scope disposed while async operation pending
await state.Exit();
scope.Dispose(); // Crash if Exit has pending callbacks

// GOOD: Wait for pending operations
await state.Exit();
await UniTask.Yield(); // Allow callbacks to complete
scope.Dispose();
```

### Problem 2: Singleton Referencing Scoped

```csharp
// BAD: Global singleton holds scoped reference
builder.Register<GlobalManager>(Lifetime.Singleton); // Root scope
builder.Register<FeatureService>(Lifetime.Scoped);   // Child scope

public class GlobalManager
{
    // This will break when child scope disposes!
    public GlobalManager(FeatureService feature) { }
}

// GOOD: Use factory or lazy resolution
public class GlobalManager
{
    private readonly IObjectResolver _resolver;
    
    public GlobalManager(IObjectResolver resolver)
    {
        _resolver = resolver;
    }
    
    public FeatureService GetFeature()
    {
        return _resolver.Resolve<FeatureService>();
    }
}
```

### Problem 3: Fire-and-Forget Without Guard

```csharp
// BAD: No cancellation when scope disposes
public async UniTaskVoid DoAsyncWork()
{
    await UniTask.Delay(5000);
    _service.DoSomething(); // May crash if disposed
}

// GOOD: Use CancellationToken
private CancellationTokenSource _cts;

public void Initialize()
{
    _cts = new CancellationTokenSource();
}

public async UniTask DoAsyncWork()
{
    await UniTask.Delay(5000, cancellationToken: _cts.Token);
    _service.DoSomething();
}

public void Dispose()
{
    _cts?.Cancel();
    _cts?.Dispose();
}
```

### Problem 4: Missing Registration

```csharp
// VContainerException: Unable to resolve IService

// Check 1: Is it registered?
builder.Register<IService, ServiceImpl>(Lifetime.Singleton);

// Check 2: Is the assembly referenced?
// Add to asmdef references

// Check 3: Is the scope correct?
// Child scope can't resolve from parent without proper hierarchy
```

### Problem 5: Circular Dependency

```csharp
// VContainerException: Circular dependency detected

// BAD
public class ServiceA
{
    public ServiceA(ServiceB b) { }
}
public class ServiceB
{
    public ServiceB(ServiceA a) { }
}

// GOOD: Break with Lazy<T> or factory
public class ServiceA
{
    private readonly Lazy<ServiceB> _b;
    public ServiceA(Lazy<ServiceB> b) => _b = b;
}
```

## Safe Resolution Patterns

### TryResolve Extension

```csharp
public static class VContainerExtensions
{
    public static bool TryResolve<T>(this IObjectResolver resolver, out T result)
    {
        result = default;
        try
        {
            if (resolver == null) return false;
            result = resolver.Resolve<T>();
            return true;
        }
        catch (VContainerException)
        {
            return false;
        }
    }
    
    public static T ResolveOrDefault<T>(this IObjectResolver resolver, T defaultValue = default)
    {
        return resolver.TryResolve<T>(out var result) ? result : defaultValue;
    }
}

// Usage
if (scope.Container.TryResolve<IState>(out var state))
{
    await state.Exit();
}
```

### Scope Alive Check

```csharp
public static bool IsAlive(this LifetimeScope scope)
{
    try
    {
        return scope != null && scope.Container != null;
    }
    catch
    {
        return false;
    }
}
```

## Testing with VContainer

### Mocking Dependencies

```csharp
[TestFixture]
public class PlayerControllerTests
{
    private LifetimeScope _testScope;
    
    [SetUp]
    public void SetUp()
    {
        _testScope = LifetimeScope.Create(builder =>
        {
            // Register mocks
            var mockInput = Substitute.For<IInputService>();
            builder.RegisterInstance(mockInput);
            
            // Register real implementation under test
            builder.Register<PlayerController>(Lifetime.Singleton);
        });
    }
    
    [TearDown]
    public void TearDown()
    {
        _testScope?.Dispose();
    }
    
    [Test]
    public void PlayerController_Move_UpdatesPosition()
    {
        var controller = _testScope.Container.Resolve<PlayerController>();
        // Test...
    }
}
```

### Testing Installers

```csharp
[Test]
public void Installer_RegistersAllDependencies()
{
    using var scope = LifetimeScope.Create(new GameInstaller());
    
    Assert.DoesNotThrow(() => scope.Container.Resolve<IPlayerService>());
    Assert.DoesNotThrow(() => scope.Container.Resolve<IInputService>());
}
```

## Editor Cleanup (Critical for Unity Editor)

```csharp
// Prevent VContainerException when exiting Play Mode
public static class EditorLifetimeScopeCleanup
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void ClearGlobalInstallers()
    {
        var type = typeof(LifetimeScope);
        var field = type.GetField("GlobalExtraInstallers", 
            BindingFlags.NonPublic | BindingFlags.Static);
        
        if (field?.GetValue(null) is IList list)
        {
            list.Clear();
        }
    }
}
```

## Best Practices

1. **Prefer constructor injection** - Explicit dependencies, easier testing
2. **Use interfaces** - Enables mocking and swapping implementations
3. **Scope appropriately** - Don't make everything Singleton
4. **Dispose child scopes** - Prevent memory leaks
5. **Use CancellationToken** - Graceful async cleanup
6. **Avoid property/field injection** - Except for optional dependencies
7. **Test your installers** - Catch missing registrations early
8. **Guard against disposed scopes** - Use TryResolve in edge cases

## Troubleshooting

| Error | Cause | Solution |
|-------|-------|----------|
| `Unable to resolve` | Not registered | Add registration |
| `Circular dependency` | A→B→A | Use Lazy<T> or factory |
| `Object disposed` | Scope disposed | Check lifecycle, add guards |
| `Multiple registrations` | Duplicate registration | Remove duplicate |
| `Invalid cast` | Wrong type registered | Check As<T>() calls |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.