Dependency Injection supplies a class with the collaborators it needs instead of making the class construct them. That shift moves object composition to a deliberate boundary, reduces coupling, makes testing practical and lets ASP.NET Core compose logging, configuration, authentication, hosted services and data access.
The Problem We Are Trying to Solve
An Order API’s OrderService directly creates a SQL repository, payment service and email notifier. It feels simple, but it couples business code to infrastructure, makes unit testing difficult, scatters configuration and creates unclear lifecycle ownership. At scale, those technical problems become slower delivery, higher regression risk and fragile releases.
public OrderService(){ _repository = new SqlOrderRepository(); }
Design evolution
Direct construction couples the caller to creation. Constructor injection — OrderService(IOrderRepository repository) — is already DI, even without a container. A composition root can manually build the graph; the built-in .NET container automates that wiring.
Dependency Injection is a design technique. A DI container is a tool that automates dependency construction and wiring.
What Is Dependency Injection?
A dependency is something a class needs to do its work. Injection means the need crosses the class boundary from outside. A container stores registrations, resolves requested services and creates the object graph. It is a composition mechanism, not a business component.
Dependency Inversion vs Dependency Injection
The Dependency Inversion Principle says high-level policy should not depend directly on low-level implementation details; both should depend on abstractions where that boundary adds value. DI is the mechanism that supplies the chosen implementation. They are related, but not identical.
| Concept | Meaning | Purpose |
|---|---|---|
| Dependency | Something a class needs | Perform work |
| Dependency Injection | Supplying it externally | Decouple construction |
| DI container | Builds the graph | Automate composition |
| Dependency Inversion | Design principle | Reduce coupling |
Why ASP.NET Core Makes DI First-Class
Controllers, Minimal APIs, logging, configuration, options, DbContext, HttpClientFactory, authentication, authorization and hosted services all integrate with DI. It is foundational to how ASP.NET Core applications are composed.
Constructor injection: my default choice
For normal required dependencies, constructor injection should generally be the default choice. It makes requirements explicit, prevents partially initialized objects, supports immutable fields and keeps tests straightforward. Method injection is useful for operation-specific dependencies. Property injection hides required dependencies and is usually avoided.
Service lifetimes: a sharing decision
Transient creates a new instance per resolution. Scoped creates one instance per scope, typically one HTTP request. Singleton creates one application-level instance shared across requests and should be used only when sharing is intentional and safe. Part 2 covers captive dependencies, disposal and thread safety in depth.
| Scenario | Recommended | Why |
|---|---|---|
| EF Core DbContext | Scoped | Request / unit of work |
| Lightweight mapper | Transient or Singleton | Depends on safety |
| Request business service | Scoped | Request lifecycle |
| In-memory lookup | Singleton when thread-safe | Shared application data |
Architect’s Choice
I start with built-in Microsoft DI and constructor injection for required dependencies. I use explicit registrations, scoped request services where appropriate, thread-safe singletons only when justified and small registration extension methods. I avoid Service Locator, static dependency access, interfaces without purpose, giant constructors, unclear lifetimes and using DI as a substitute for architecture.
Common mistakes
- Everything Singleton.
- Everything Scoped.
newinside services.- Injecting
IServiceProvidereverywhere. - Service Locator.
- Too many constructor dependencies.
- Interface for every class.
- Business logic in registration.
- Hidden resolution.
- Lifetime mismatch.
Testing with DI
var repository = new FakeOrderRepository(); var service = new OrderService(repository);
DI makes dependencies replaceable, but it does not automatically make code testable. Classes still need cohesive responsibilities and useful boundaries.
Dependency Injection Code Review Checklist
- ☐ Dependencies are injected rather than created internally.
- ☐ Constructor injection is used for mandatory dependencies.
- ☐ The lifetime matches state and ownership.
- ☐ Singleton services are safe for concurrent use.
- ☐ Each interface adds architectural value.
- ☐ Constructor dependencies are cohesive and reasonably few.
- ☐
IServiceProvideris not used as Service Locator. - ☐ Registrations are organized but discoverable.
- ☐ Disposable resources are owned correctly.
- ☐ Request state is not accidentally stored in a singleton.
- ☐ Dependency direction matches the architecture.
DI is about dependency ownership and composition. Constructor injection is the default. Built-in .NET DI is sufficient for most applications. Lifetime is an architectural decision. Interfaces should represent meaningful boundaries. DI enables good design, but cannot compensate for poor design.
Continue to Part 2
Dependency Injection in .NET — Part 2: Internals, Advanced Patterns & Production Considerations will cover ServiceProvider resolution, scopes, captive dependencies, disposal, open generics, keyed services, factories, BackgroundService scopes, performance, troubleshooting and AKS considerations.
