Dependency Injection in .NET
The problem
A class that directly creates the concrete objects it depends on is locked to those specific implementations, making it hard to test in isolation or swap in a different implementation later — dependency injection hands a class its dependencies from outside instead of letting it construct them itself.
Why now
SOLID's Dependency Inversion principle already established depending on abstractions instead of concrete classes as a goal; dependency injection is the concrete mechanism for actually achieving that at scale, and coupling and cohesion already gave the general vocabulary (low coupling) this pattern directly serves.
Mental model
Instead of a class saying 'give me a database connection, I'll create it myself,' dependency injection has the class declare 'I need something that implements IDatabase' and receive a concrete instance from outside — typically from a DI container that's already been configured with which concrete class to hand out for each abstraction. This is exactly the creational-pattern instinct (isolate concrete construction in one place) applied at the scale of an entire application instead of one class.
Requires
- Coupling & CohesionSoftware Architecture
Dependency injection exists specifically to reduce coupling between a class and its dependencies' concrete implementations; you need that general goal already established before the specific technique for achieving it is motivated.
- SOLID PrinciplesLow-Level Design
Dependency injection is the practical, everyday mechanism .NET applications use to satisfy the Dependency Inversion Principle; you need that principle stated as a goal before a container that automates satisfying it makes sense as more than extra machinery.
Unlocks
Used in
- ASP.NET Core's built-in dependency injection container, used throughout every layer of a typical application
- Unit testing, where injected dependencies can be replaced with test doubles/mocks
- Service lifetime management (singleton, scoped, transient) controlling how long an injected instance lives
- Swapping implementations (a real payment gateway for a test one) without changing any consuming code
Projects
- Design a service class that depends on an abstraction (interface) rather than a concrete class, register both a real and a fake implementation with a DI container, and swap between them via configuration
- Explain the difference between singleton, scoped, and transient service lifetimes with a concrete example of when each is appropriate in a web application
Examples
- A controller depending on IEmailService rather than a concrete SmtpEmailService can be tested with a fake email service that just records calls, without ever sending a real email
- A scoped service lifetime in ASP.NET Core creates one instance per HTTP request — appropriate for something like a database context that shouldn't be shared across unrelated requests
Resources
Mastery checklist
- I can design a class that depends on an abstraction and receives its implementation via injection
- I can register services with a DI container and explain singleton, scoped, and transient lifetimes
- I can swap a real implementation for a test double without modifying the consuming class
- I can explain how dependency injection satisfies the Dependency Inversion Principle in practice