Singleton vs Scoped vs Transient in C#: Key Differences and Usage
Singleton creates one instance for the entire application lifetime, Scoped creates one instance per client request or scope, and Transient creates a new instance every time it is requested. These lifetimes control how objects are shared and reused in dependency injection.Quick Comparison
Here is a quick table comparing Singleton, Scoped, and Transient lifetimes in C# dependency injection.
| Aspect | Singleton | Scoped | Transient |
|---|---|---|---|
| Instance Count | One instance for entire app | One instance per scope/request | New instance every time |
| Lifetime | Application lifetime | Scope lifetime (e.g., HTTP request) | Very short, per injection |
| Use Case | Shared services, config | Per user/request data | Lightweight, stateless services |
| Performance | Best for expensive objects | Moderate | Can be costly if overused |
| State | Shared state across app | State isolated per scope | No shared state |
| Example | Logging service | Database context | Email sender |
Key Differences
Singleton means the service is created once and reused everywhere. It lives as long as the application runs, so all parts of the app share the same instance. This is good for services that hold shared data or are expensive to create.
Scoped creates a new instance for each scope, commonly an HTTP request in web apps. This means each user request gets its own instance, preventing data leaks between requests. Scoped services are useful for things like database contexts where you want to track changes per request.
Transient creates a new instance every time the service is requested. This is useful for lightweight, stateless services where you want a fresh object each time. However, overusing transient services can hurt performance because of frequent object creation.
Code Comparison
Here is how you register and use a Singleton service in C# with dependency injection.
public interface ILoggerService { void Log(string message); } public class LoggerService : ILoggerService { public LoggerService() { Console.WriteLine("LoggerService created"); } public void Log(string message) { Console.WriteLine($"Log: {message}"); } } // In Startup.cs or Program.cs var services = new ServiceCollection(); services.AddSingleton<ILoggerService, LoggerService>(); var provider = services.BuildServiceProvider(); var logger1 = provider.GetService<ILoggerService>(); var logger2 = provider.GetService<ILoggerService>(); logger1.Log("First message"); logger2.Log("Second message"); Console.WriteLine(Object.ReferenceEquals(logger1, logger2));
Scoped Equivalent
Here is how you register and use a Scoped service in C# with dependency injection.
public interface IDbContext { Guid GetOperationId(); } public class DbContext : IDbContext { private Guid _operationId; public DbContext() { _operationId = Guid.NewGuid(); Console.WriteLine($"DbContext created with ID: {_operationId}"); } public Guid GetOperationId() => _operationId; } // In Startup.cs or Program.cs var services = new ServiceCollection(); services.AddScoped<IDbContext, DbContext>(); var provider = services.BuildServiceProvider(); using (var scope1 = provider.CreateScope()) { var db1 = scope1.ServiceProvider.GetService<IDbContext>(); var db2 = scope1.ServiceProvider.GetService<IDbContext>(); Console.WriteLine(db1.GetOperationId() == db2.GetOperationId()); // True } using (var scope2 = provider.CreateScope()) { var db3 = scope2.ServiceProvider.GetService<IDbContext>(); Console.WriteLine(db3.GetOperationId()); }
When to Use Which
Choose Singleton when you need one shared instance for the whole app, like logging or configuration services that are expensive to create and safe to share.
Choose Scoped when you want a new instance per user request or operation, such as database contexts that track changes per request and should not be shared across requests.
Choose Transient when you want a new instance every time, ideal for lightweight, stateless services that do not hold data and are cheap to create.