Dependency Injection (Singleton, Scoped, Transient) Masterclass
Welcome to Phase 11 (Chapter 31): C# Dependency Injection, Service Lifetimes & Interface-Based Design Masterclass! Dependency Injection (DI) is a software design pattern where objects receive their dependencies from external sources rather than creating them internally. .NET's built-in IoC container manages object creation, lifetime, and disposal automatically. In this chapter, we cover tight vs loose coupling, service registration, Singleton, Scoped, Transient lifetimes, constructor injection, and interface-based design for testable code.
Without DI (Tight Coupling): Classes directly instantiate their own dependencies using new, making them impossible to swap, test, or mock independently.
With DI (Loose Coupling): Dependencies are injected through constructor parameters as interface abstractions, enabling easy swapping of implementations and mock injection during unit tests.
// โ TIGHT COUPLING โ Hard to test or swap EmailService
public class OrderService
{
private EmailService emailService = new EmailService(); // Direct creation!
public void PlaceOrder(string product)
{
emailService.SendConfirmation(product);
}
}
// โ
LOOSE COUPLING โ Constructor Injection via Interface
public interface IEmailService
{
void SendConfirmation(string product);
}
public class EmailService : IEmailService
{
public void SendConfirmation(string product)
{
Console.WriteLine($"Email sent for order: {product}");
}
}
public class OrderService
{
private readonly IEmailService _emailService; // Depends on INTERFACE, not concrete class
public OrderService(IEmailService emailService) // Injected via constructor
{
_emailService = emailService;
}
public void PlaceOrder(string product) => _emailService.SendConfirmation(product);
}
| Lifetime | Registration Method | Instance Creation Strategy | Best Used For |
|---|---|---|---|
| Singleton | AddSingleton<I, T>() | ONE instance created for the entire app lifetime | Config services, caches, DB connection pools |
| Scoped | AddScoped<I, T>() | ONE instance per HTTP request scope | Entity Framework DbContext, per-request services |
| Transient | AddTransient<I, T>() | NEW instance every time it is requested | Lightweight stateless services, email senders |
var builder = WebApplication.CreateBuilder(args);
// Register services with their lifetimes
builder.Services.AddSingleton<IConfigService, AppConfigService>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddTransient<IEmailService, SmtpEmailService>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.MapControllers();
app.Run();
Q1: What is an IoC Container?
An Inversion of Control (IoC) Container is an engine that manages object creation and dependency wiring automatically. .NET's built-in IServiceCollection is the IoC container.
Q2: What is the Captive Dependency problem?
It occurs when a Singleton service depends on a Scoped or Transient service, capturing a short-lived service inside a long-lived container. This causes stale state bugs. Always inject only same-lifetime or longer-lived services into Singleton.