Hello there!ππ§ββοΈ Today, we're exploring the three main service lifetimes: singleton, scoped, and transient.
Understanding dependency injection (DI) lifecycle management is crucial for building reliable applications. Get it wrong, and you might end up with memory leaks or shared state bugs. Get it right, and your code will be cleaner and easier to maintain!
Overview
When you register a service in a dependency injection container, you need to specify its *lifetime, how long the container should keep an instance alive. The three main lifetimes are:
- Singleton - One instance for the entire application lifetime
- Scoped - One instance per scope (typically per HTTP request in web apps)
- Transient - A new instance every time it's requested
1. Singleton
What is Singleton?
Singleton is like having one shared tool that everyone uses. There's only one instance, and it lives for the entire lifetime of your application.
Key Characteristics:
- One instance for the entire application
- Shared across all requests
- Created once when first requested
- Disposed when the application shuts down
When to Use Singleton
Use singleton for:
- β Stateless services - Services that don't hold instance-specific data
- β Expensive to create - Services costly to initialize (caches, HTTP clients)
- β Shared resources - Configuration, logging, caching
Example
// Register as singleton
services.AddSingleton<ICacheService, CacheService>();
services.AddSingleton<ILogger, Logger>();
// Example: Cache service
public class CacheService : ICacheService
{
private readonly Dictionary<string, object> _cache = new();
private readonly object _lock = new object();
public void Set<T>(string key, T value)
{
lock (_lock) { _cache[key] = value; }
}
public T Get<T>(string key)
{
lock (_lock)
{
return _cache.TryGetValue(key, out var value) ? (T)value : default(T);
}
}
}
Common Pitfalls
β οΈ Watch out for:
- Shared State Issues
// BAD: Singleton with mutable state
public class BadService
{
public string CurrentUserId { get; set; } // Shared across all requests!
}
- Thread Safety - Ensure thread safety if shared state is needed
-
Disposal - Implement
IDisposableif holding resources
2. Scoped
What is Scoped?
Scoped is like having a personal workspace for each task. In web applications, each HTTP request gets its own scope, and all scoped services share the same instance within that scope.
Key Characteristics:
- One instance per scope (typically per HTTP request)
- Shared within the same scope, different across scopes
- Created when the scope begins
- Disposed when the scope ends
When to Use Scoped
Use scoped for:
- β Database contexts - Entity Framework DbContext
- β Request-specific services - Services that need state during a request
- β Unit of Work patterns - Services coordinating multiple operations
Example
// Register as scoped
services.AddScoped<IDbContext, ApplicationDbContext>();
services.AddScoped<IOrderService, OrderService>();
// Example: Database context
public class ApplicationDbContext : DbContext
{
public DbSet<Order> Orders { get; set; }
}
public class OrderService
{
private readonly ApplicationDbContext _context;
public OrderService(ApplicationDbContext context) =>
_context = context; // Same instance throughout the request
public async Task<Order> CreateOrder(Order order)
{
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return order;
}
}
Common Pitfalls
β οΈ Watch out for:
- Using Scoped Services in Singleton
// BAD: Singleton depending on scoped service
public class BadSingleton
{
private readonly IScopedService _scoped; // Error!
}
// GOOD: Use IServiceProvider
public class GoodSingleton
{
private readonly IServiceProvider _serviceProvider;
public GoodSingleton(IServiceProvider serviceProvider) =>
_serviceProvider = serviceProvider;
public void DoWork()
{
using (var scope = _serviceProvider.CreateScope())
{
var scoped = scope.ServiceProvider.GetRequiredService<IScopedService>();
scoped.DoWork();
}
}
}
- Capturing Scoped Services - Don't capture scoped services in background tasks without proper scope management
3. Transient
What is Transient?
Transient is like getting a fresh tool every time you need one. Each time you request a transient service, you get a brand new instance. Important: Transient instances are NOT kept alive for the app lifetime, they're created on-demand and disposed when they go out of scope (garbage collected).
Key Characteristics:
- New instance every time it's requested
- No sharing between consumers
- Created on demand
- Disposed when out of scope (garbage collected, NOT kept for app lifetime)
- Shortest lifetime of all three options
When to Use Transient
Use transient for:
- β Lightweight services - Services that are cheap to create
- β Stateless operations - Services that don't maintain state
- β Services that shouldn't be shared - When sharing would cause issues
Example
// Register as transient
services.AddTransient<IEmailService, EmailService>();
services.AddTransient<IValidator<Order>, OrderValidator>();
// Example: Validator service
public class OrderValidator : IValidator<Order>
{
public ValidationResult Validate(Order order)
{
var result = new ValidationResult();
if (order.Items == null || order.Items.Count == 0)
result.AddError("Order must have at least one item");
return result;
}
}
Common Pitfalls
β οΈ Watch out for:
- Performance Issues - Don't use transient for expensive-to-create services
- Memory Leaks - Don't hold static references in transient services
- Unnecessary Creation - Don't use transient when singleton would work
Comparison Table
| Aspect | Singleton | Scoped | Transient |
|---|---|---|---|
| Instance Count | One for entire app | One per scope | New every time |
| Lifetime | Application lifetime | Scope lifetime (per request) | Shortest - disposed when out of scope |
| Memory Usage | Low (shared) | Medium | High (many instances created) |
| Thread Safety | Must be thread-safe | Usually not needed | Usually not needed |
| Disposal | When app shuts down | When scope ends | When garbage collected |
| Use Case | Stateless, expensive | Request-specific | Lightweight, stateless |
Common Scenarios
Web Application
public void ConfigureServices(IServiceCollection services)
{
// Singleton: Shared across all requests
services.AddSingleton<ICacheService, CacheService>();
// Scoped: One per HTTP request
services.AddScoped<IDbContext, ApplicationDbContext>();
// Transient: New instance each time
services.AddTransient<IEmailService, EmailService>();
}
Background Service
public class BackgroundWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
public BackgroundWorker(IServiceProvider serviceProvider) =>
_serviceProvider = serviceProvider;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Create a scope for each iteration
using (var scope = _serviceProvider.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<IScopedService>();
await scopedService.DoWorkAsync();
}
await Task.Delay(1000, stoppingToken);
}
}
}
Dependency Rules
A service can only depend on services with equal or longer lifetimes.
- β Singleton can depend on: Singleton
- β Scoped can depend on: Singleton, Scoped
- β Transient can depend on: Singleton, Scoped, Transient
Common Mistake:
// BAD: Singleton depending on scoped service
public class SingletonService
{
private readonly IScopedService _scoped; // Error!
}
// GOOD: Use IServiceProvider to create scope when needed
public class SingletonService
{
private readonly IServiceProvider _serviceProvider;
public SingletonService(IServiceProvider serviceProvider) =>
_serviceProvider = serviceProvider;
public void DoWork()
{
using (var scope = _serviceProvider.CreateScope())
{
var scoped = scope.ServiceProvider.GetRequiredService<IScopedService>();
scoped.DoWork();
}
}
}
Best Practices
1. Choose the Right Lifetime
- Singleton: Stateless, expensive, shared resources
- Scoped: Request-specific, database contexts
- Transient: Lightweight, stateless, fresh instance needed
2. Follow Dependency Rules
- Never have shorter lifetimes depend on longer ones
- Use
IServiceProviderwhen you need to break the rules - Understand the implications of each choice
3. Handle Disposal Properly
// Implement IDisposable for services that hold resources
public class ResourceService : IDisposable
{
private bool _disposed = false;
public void Dispose()
{
if (_disposed) return;
// Clean up resources
_disposed = true;
}
}
Conclusion
Understanding dependency injection lifetimes is crucial for building robust applications:
Singleton - One instance for the entire application:
- Use for stateless, expensive-to-create services
- Ensure thread safety
- Great for caching, logging, configuration
Scoped - One instance per scope (request):
- Use for request-specific services
- Perfect for database contexts
- Automatically managed in ASP.NET Core
Transient - New instance every time:
- Use for lightweight, stateless services
- When you need a fresh instance each time
- NOT kept alive for app lifetime - instances are created and disposed quickly
- Be mindful of performance implications (many instances created)
The Golden Rules:
- Choose the shortest lifetime that meets your needs
- Follow dependency lifetime rules (shorter can't depend on longer)
- Implement
IDisposablefor resource cleanup - Test and monitor your choices
Remember, there's no one-size-fits-all answer. The right choice depends on your specific use case and performance requirements. Start with the most restrictive lifetime that works, and adjust as needed.
You've got this! πͺ Happy coding!
Top comments (0)