-
Notifications
You must be signed in to change notification settings - Fork 0
Lifetimes
Service lifetimes control how long instances live and whether they're shared between resolutions.
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 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)- 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 instances are not automatically disposed by the container. You must manage their lifecycle:
var enemy = container.Locate<Enemy>();
// Use enemy...
enemy.Dispose(); // Manual disposal requiredSingleton 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)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- 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();
});Register existing instances as singletons:
var config = LoadConfiguration();
container.Configure(c =>
{
c.ExportInstance(config).As<IConfiguration>();
});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 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)- 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 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 hereSee Scopes for more details on scope management.
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();
});| 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.
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();
});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)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);
}
}- Scopes - Deep dive into scope management
- Registration - Registration options
- Performance - Lifetime performance characteristics