Skip to content

Lifetimes

Jim Burlison edited this page Dec 5, 2025 · 1 revision

Lifetimes

Service lifetimes control how long instances live and whether they're shared between resolutions.

Lifetime Types

SSDI supports three lifetime modes:

Lifetime Instance Created Shared Disposal
Transient Every resolution No Manual
Singleton First resolution Yes, globally Container disposal
Scoped First resolution per scope Yes, within scope Scope disposal

Transient (Default)

Transient services create a new instance every time they're resolved.

container.Configure(c =>
{
    c.Export<Enemy>();  // Transient is default
    // OR explicitly:
    c.Export<Enemy>().Lifestyle.Transient();
});

var enemy1 = container.Locate<Enemy>();
var enemy2 = container.Locate<Enemy>();
// enemy1 != enemy2 (different instances)

When to Use Transient

  • Stateless services
  • Short-lived objects
  • Objects that maintain request-specific state
  • Factory-created objects like game entities
container.Configure(c =>
{
    c.Export<Projectile>();      // Each bullet is unique
    c.Export<ParticleEffect>();  // Each effect is independent
    c.Export<DamageCalculator>(); // Stateless calculation
});

Transient Disposal

Transient instances are not automatically disposed by the container. You must manage their lifecycle:

var enemy = container.Locate<Enemy>();
// Use enemy...
enemy.Dispose(); // Manual disposal required

Singleton

Singleton services create one instance for the entire application lifetime.

container.Configure(c =>
{
    c.Export<GameEngine>().Lifestyle.Singleton();
});

var engine1 = container.Locate<GameEngine>();
var engine2 = container.Locate<GameEngine>();
// engine1 == engine2 (same instance)

Lazy Initialization

Singletons are created lazily on first resolution, not at registration time:

container.Configure(c =>
{
    c.Export<ExpensiveService>().Lifestyle.Singleton();
});

// ExpensiveService constructor has NOT been called yet

var service = container.Locate<ExpensiveService>();
// NOW the constructor is called

var service2 = container.Locate<ExpensiveService>();
// Returns cached instance, no constructor call

When to Use Singleton

  • Configuration and settings
  • Shared state managers
  • Resource pools
  • Caches
  • Core engine components
container.Configure(c =>
{
    c.Export<GameEngine>().Lifestyle.Singleton();
    c.Export<ConfigurationManager>().Lifestyle.Singleton();
    c.Export<AssetCache>().Lifestyle.Singleton();
    c.Export<NetworkManager>().Lifestyle.Singleton();
});

Pre-Built Singletons

Register existing instances as singletons:

var config = LoadConfiguration();

container.Configure(c =>
{
    c.ExportInstance(config).As<IConfiguration>();
});

Thread Safety

Singleton resolution is thread-safe. The same instance is returned regardless of which thread resolves it:

// Safe to call from multiple threads
Parallel.For(0, 100, i =>
{
    var engine = container.Locate<GameEngine>();
    // All 100 iterations get the same instance
});

Scoped

Scoped services create one instance per scope. Different scopes get different instances.

container.Configure(c =>
{
    c.Export<PlayerInventory>().Lifestyle.Scoped();
});

using var scope1 = container.CreateScope();
using var scope2 = container.CreateScope();

var inv1a = scope1.Locate<PlayerInventory>();
var inv1b = scope1.Locate<PlayerInventory>();
var inv2 = scope2.Locate<PlayerInventory>();

// inv1a == inv1b (same scope)
// inv1a != inv2  (different scopes)

When to Use Scoped

  • Per-request services (web applications)
  • Per-player services (game servers)
  • Per-session services
  • Per-transaction services
container.Configure(c =>
{
    // Per-player services
    c.Export<PlayerInventory>().Lifestyle.Scoped();
    c.Export<PlayerStats>().Lifestyle.Scoped();
    c.Export<PlayerQuests>().Lifestyle.Scoped();
});

// When player connects
using var playerScope = container.CreateScope();
var inventory = playerScope.Locate<PlayerInventory>();
var stats = playerScope.Locate<PlayerStats>();

// Player uses these throughout their session...

// When player disconnects - automatic cleanup
playerScope.Dispose();

Scoped Disposal

Scoped services implementing IDisposable or IAsyncDisposable are automatically disposed when the scope ends:

public class DatabaseConnection : IDisposable
{
    public void Dispose() => Console.WriteLine("Connection closed");
}

container.Configure(c =>
{
    c.Export<DatabaseConnection>().Lifestyle.Scoped();
});

using (var scope = container.CreateScope())
{
    var conn = scope.Locate<DatabaseConnection>();
    // Use connection...
} // "Connection closed" printed here

See Scopes for more details on scope management.

Mixed Lifetimes

Services can depend on other services with different lifetimes:

public class PlayerService
{
    private readonly ILogger _logger;        // Singleton
    private readonly PlayerData _data;       // Scoped

    public PlayerService(ILogger logger, PlayerData data)
    {
        _logger = logger;
        _data = data;
    }
}

container.Configure(c =>
{
    c.Export<ConsoleLogger>().As<ILogger>().Lifestyle.Singleton();
    c.Export<PlayerData>().Lifestyle.Scoped();
    c.Export<PlayerService>().Lifestyle.Scoped();
});

Lifetime Rules

Dependent Can Depend On
Transient Transient, Scoped, Singleton
Scoped Scoped, Singleton
Singleton Singleton only*

*Singletons depending on scoped/transient services will capture a single instance, which may not be the intended behavior.

Captive Dependency Warning

Be careful with singleton services that depend on shorter-lived services:

// ⚠️ Problematic - singleton captures transient
public class GameEngine  // Singleton
{
    private readonly ILogger _logger;  // If transient, this instance is "captured"
}

// ✅ Better - singleton depends on singleton
container.Configure(c =>
{
    c.Export<ConsoleLogger>().As<ILogger>().Lifestyle.Singleton();
    c.Export<GameEngine>().Lifestyle.Singleton();
});

Lifetime Comparison Example

container.Configure(c =>
{
    c.Export<TransientCounter>().Lifestyle.Transient();
    c.Export<SingletonCounter>().Lifestyle.Singleton();
    c.Export<ScopedCounter>().Lifestyle.Scoped();
});

// Transient - always new
var t1 = container.Locate<TransientCounter>(); // Instance #1
var t2 = container.Locate<TransientCounter>(); // Instance #2

// Singleton - always same
var s1 = container.Locate<SingletonCounter>(); // Instance #1
var s2 = container.Locate<SingletonCounter>(); // Instance #1 (same)

// Scoped - same within scope
using var scope = container.CreateScope();
var sc1 = scope.Locate<ScopedCounter>(); // Instance #1
var sc2 = scope.Locate<ScopedCounter>(); // Instance #1 (same)

using var scope2 = container.CreateScope();
var sc3 = scope2.Locate<ScopedCounter>(); // Instance #2 (new scope)

Game Server Pattern

A complete example showing all three lifetimes in a game server:

var container = new DependencyInjectionContainer();

container.Configure(c =>
{
    // SINGLETONS - Shared across entire server
    c.Export<GameWorld>().Lifestyle.Singleton();
    c.Export<NetworkManager>().Lifestyle.Singleton();
    c.Export<AssetManager>().Lifestyle.Singleton();
    c.Export<ConfigurationManager>().Lifestyle.Singleton();

    // SCOPED - Per-player services
    c.Export<PlayerInventory>().Lifestyle.Scoped();
    c.Export<PlayerStats>().Lifestyle.Scoped();
    c.Export<PlayerPosition>().Lifestyle.Scoped();
    c.Export<PlayerConnection>().Lifestyle.Scoped();

    // TRANSIENT - Created frequently, short-lived
    c.Export<ChatMessage>();
    c.Export<Projectile>();
    c.Export<DamageEvent>();
    c.Export<NetworkPacket>();
});

// Server startup
var world = container.Locate<GameWorld>();
var network = container.Locate<NetworkManager>();

// Player connects
void OnPlayerConnect(PlayerId id)
{
    var playerScope = container.CreateScope();
    _playerScopes[id] = playerScope;

    var inventory = playerScope.Locate<PlayerInventory>();
    var stats = playerScope.Locate<PlayerStats>();
    // Initialize player...
}

// Player disconnects
void OnPlayerDisconnect(PlayerId id)
{
    if (_playerScopes.TryGetValue(id, out var scope))
    {
        scope.Dispose(); // Cleanup all player resources
        _playerScopes.Remove(id);
    }
}

Next Steps

Clone this wiki locally