Lesson 27: Dependency Injection Internals in .NET
Understand how .NET's built-in DI container works — service lifetimes, constructor injection, and building testable applications.
What You'll Learn
- • Why DI matters and how it decouples your code
- • .NET's ServiceCollection and ServiceProvider
- • Service lifetimes: Singleton, Scoped, Transient
- • Constructor injection and automatic resolution
🧠 Real-World Analogy
DI is like a restaurant kitchen. A chef (your class) doesn't go to the farm for ingredients — ingredients (dependencies) are delivered to the kitchen. The restaurant manager (DI container) decides which supplier to use. During testing, you swap real suppliers for mock ones.
DI Basics — Constructor Injection
Without DI, your classes create their own dependencies with new, making them impossible to test or swap. With DI, dependencies are passed in through the constructor — the class doesn't care which implementation it gets.
Constructor Injection vs Tight Coupling
Compare tightly-coupled code with properly injected dependencies.
using System;
// Without DI — tightly coupled (BAD)
class BadOrderService
{
private readonly SqlDatabase _db = new SqlDatabase(); // Hard dependency!
public void PlaceOrder(string item) => _db.Save($"Order: {item}");
}
class SqlDatabase
{
public void Save(string data) => Console.WriteLine($"SQL: Saved '{data}'");
}
// With DI — loosely coupled (GOOD)
interface IDatabase
{
void Save(string data);
}
class PostgresDatabase : IDatabase
{
public void Save(string data) => Conso
...The .NET DI Container
ServiceCollection is .NET's built-in DI container. You register services with their interfaces, build a ServiceProvider, and the container automatically resolves the entire dependency graph — including nested dependencies.
ServiceCollection & Auto-Resolution
Build a DI container that resolves nested dependencies automatically.
using System;
using Microsoft.Extensions.DependencyInjection;
// Service interfaces
interface IEmailSender
{
void Send(string to, string body);
}
interface ILogger
{
void Log(string message);
}
interface IUserService
{
void Register(string email);
}
// Implementations
class SmtpEmailSender : IEmailSender
{
public void Send(string to, string body) =>
Console.WriteLine($" 📧 SMTP → {to}: {body}");
}
class ConsoleLogger : ILogger
{
public void Log(string message) =
...Service Lifetimes
Choosing the right lifetime is critical. Use the wrong one and you'll get bugs that are incredibly hard to track down.
| Lifetime | Created | Use For |
|---|---|---|
| Singleton | Once, shared forever | Caches, configuration, loggers |
| Scoped | Once per scope (HTTP request) | DbContext, per-request services |
| Transient | Every time it's requested | Lightweight, stateless services |
Singleton vs Scoped vs Transient
See how each lifetime behaves across scopes and requests.
using System;
using Microsoft.Extensions.DependencyInjection;
class SingletonService
{
public Guid Id { get; } = Guid.NewGuid();
public SingletonService() => Console.WriteLine($" Singleton created: {Id}");
}
class ScopedService
{
public Guid Id { get; } = Guid.NewGuid();
public ScopedService() => Console.WriteLine($" Scoped created: {Id}");
}
class TransientService
{
public Guid Id { get; } = Guid.NewGuid();
public TransientService() => Console.WriteLine($" Transien
...Pro Tip
Enable ValidateScopes and ValidateOnBuild in development to catch lifetime mismatches early: services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true });
Common Mistakes
- • Injecting Scoped into Singleton — the scoped service becomes a singleton (captive dependency)
- • Using
newinside DI-registered classes — bypasses the container - • Registering everything as Singleton — causes stale data and threading issues
Lesson Complete!
You now understand .NET's DI system inside and out. Next, learn how to implement structured logging with ILogger and Serilog.
Sign up for free to track which lessons you've completed and get learning reminders.