dependency-injection-in-net-part-1-fundamentals-design-decisions-implementation
← Back to Series
HOME / SOFTWARE ARCHITECTURE / .NET CORE FEATURES / FEATURE #01 / PART 1 OF 2
.NET CORE FEATURES · FEATURE #01 · PART 1 OF 2

Dependency Injection in .NET — Part 1: Fundamentals, Design Decisions & Implementation

From tightly coupled code to maintainable, testable and production-ready application design.

SHYAM SUNDER SINGH · 25 MIN READ · SEP 1, 2026

THE ARCHITECTURE QUESTION

Who owns the object graph?

Dependency Injection is less about a framework feature and more about making ownership, boundaries and change explicit.

DI
01
Executive summary

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.

Dependency injection object composition flow A four-step flow from registrations to a resolved OrderService object graph. From registration to a ready object graph The container automates composition; the design still belongs to the application. 01 · REGISTER IOrderRepository → SqlOrderRepository IOrderService → OrderService Composition root 02 · RESOLVE IOrderService requested by endpoint Container finds OrderService 03 · BUILD OrderService SqlOrderRepository 04 · INJECT Ready service constructor receives its requirements No service locator. DI is the technique · the container is the automation tool
From registration to a ready object graphDI technique versus container automation.

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.

Dependency Injection in an ASP.NET Core Application A C4-inspired diagram showing a customer request entering an ASP.NET Core Order API, with the DI container composing application and infrastructure services. Dependency Injection in an ASP.NET Core Application Request flow and object composition are related — but they are not the same direction. ASP.NET CORE ORDER API — SYSTEM BOUNDARY Customer requester Web / Mobile App HTTP client ASP.NET Core Order API Controllers / Minimal APIs receives the request REQUEST FLOW DI Container composition mechanism registration → resolution → graph not a business component RESOLVES Application Layer OrderService Payment Service IPaymentService Notification Service IEmailSender Infrastructure: SqlOrderRepository → Database Solid arrows: request / dependency flow Dashed arrows: container composition responsibility
Dependency Injection in an ASP.NET Core ApplicationRequest flow versus composition responsibility.

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.

.NET DI service lifetime visualization Three columns compare transient, scoped, and singleton instances across resolutions and requests. Service lifetimes are sharing decisions The important question is not “which lifetime is shortest?” — it is “who should share this instance, and for how long?” Transient new instance per resolution Scoped one instance per scope / request Singleton one instance for the app lifetime REQUEST A / RESOLUTION 1 #1 new object REQUEST A / RESOLUTION 2 #2 different object REQUEST A OrderService #1 REQUEST B OrderService #2 A new request gets a new instance. Shared instance #1 Request A Request B Request C Only when safe to share.
Service lifetimes are sharing decisionsInstance reuse across resolutions and requests.
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
Request-specific state? YES → SCOPED Safe to share concurrently? NO → TRANSIENT / SCOPED Creation expensive? YES → CONSIDER SINGLETON

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

  1. Everything Singleton.
  2. Everything Scoped.
  3. new inside services.
  4. Injecting IServiceProvider everywhere.
  5. Service Locator.
  6. Too many constructor dependencies.
  7. Interface for every class.
  8. Business logic in registration.
  9. Hidden resolution.
  10. 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.
  • IServiceProvider is 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.
Architect’s Takeaway

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.