Dependency Injection

By the end of this lesson you'll be able to decouple your classes by injecting their dependencies through constructors, wire a service graph by hand and with .NET's built-in container, choose the right service lifetime, and write code that's genuinely easy to test.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Dependency injection is like a power socket on the wall . Your kettle, laptop charger, and lamp all plug into the same standard socket — they don't care which power station generated the electricity, whether it's a coal plant, a wind farm, or a battery in your garage. They just depend on the interface (the socket shape and voltage). The grid can switch suppliers behind the wall and your appliances never notice. Constructor injection is exactly this: a class declares the "socket" it needs (an interface in its constructor), and whoever builds the class plugs in whichever implementation they like — the real one in production, a fake one in tests. An IoC container is the building's electrician: register what plugs into what once, and it wires every socket for you.

A dependency is any object your class needs to do its job — a logger, a database, an email sender. Most beginner code creates those dependencies itself with new , hard-wiring the class to one exact implementation. Dependency injection (DI) flips that around: instead of a class making its collaborators, it is given them from outside.

This is one half of the Dependency Inversion Principle — the "D" in SOLID: high-level code should depend on abstractions (interfaces), not on concrete low-level classes. The benefit is loose coupling : you can replace, mock, or upgrade a dependency without editing the class that uses it.

There's one more idea bundled in: Inversion of Control (IoC) . Normally your code controls when objects are created. With an IoC container you hand that control over — the container decides when to build each service and how long it lives. This lesson builds up in exactly that order: inject by hand first, then let a container do it.

Rule of thumb: when in doubt, choose Transient — it's the safest default because a fresh instance can never hold stale state. Reach for Scoped or Singleton only when you have a concrete reason to share an instance.

1. Constructor Injection — The Core Idea

When a class creates its own dependency with new , it is tightly coupled to that exact implementation — you can't swap it, and you can't test the class in isolation. Constructor injection fixes this: the class declares the interface it needs as a constructor parameter, and the caller passes in a concrete implementation. The class depends on the contract (the interface ), never the concrete type. Read this worked example, run it, then you'll do the refactor yourself.

Your turn. The Greeter below needs an IMessageService , but the constructor isn't finished. Add the parameter and store it, then wire it by hand in Main . Fill in the three ___ blanks, then run it.

2. Swapping Implementations — The Payoff

Here's why the effort is worth it. Because Greeter depends on the IMessageService interface and not a concrete class, you can hand it a completely different implementation without changing a single line of Greeter . That's loose coupling in action — and it's the same trick that lets you inject a fake service during testing. Swap the implementation in the two blanks below.

3. The IoC Container — Wiring Done For You

Wiring two classes by hand is easy. Wiring fifty, each depending on several others, is not — you'd be writing pages of new in the right order. An IoC container solves this: you register each interface against its implementation once with AddSingleton / AddScoped / AddTransient , call BuildServiceProvider() , then ask for a service with GetService<T>() . The container inspects each constructor and supplies the whole dependency graph automatically. .NET ships one built in — ServiceCollection from Microsoft.Extensions.DependencyInjection .

Note: GetService<T>() returns null if a service wasn't registered; GetRequiredService<T>() throws a clear exception instead. In real apps, prefer GetRequiredService so a missing registration fails loudly.

4. Service Lifetimes — Singleton, Scoped, Transient

When you register a service you choose how long each instance lives. A Singleton is created once and shared for the entire app. A Scoped service is created once per scope — in a web app, that's typically one HTTP request. A Transient service is built fresh every single time it's resolved. Picking the wrong one causes subtle, hard-to-find bugs, so it's worth seeing the difference. Each service below stamps itself with a unique id so you can tell instances apart.

The most common lifetime bug is a captive dependency : a long-lived service that holds a reference to a shorter-lived one. If a Singleton takes a Scoped service in its constructor, the container injects it once at startup — and that scoped instance is now trapped inside the singleton forever , never refreshed per request. The classic disaster is a singleton accidentally pinning a DbContext (which is scoped), leaking data and connections across requests.

The rule: a service may only depend on something with an equal or longer lifetime. Singleton → Singleton is fine; Singleton → Scoped is the trap.

.NET can catch many of these for you: build the provider with ValidateScopes = true (and ValidateOnBuild = true ) in development and it throws at startup instead of misbehaving at runtime.

5. Why DI Makes Code Testable

This is the payoff that wins teams over. Because a class receives its dependencies through its constructor, a unit test can pass in a fake — an in-memory stand-in you fully control — instead of the real database, payment gateway, or email server. No network, no slow setup, and you can inspect exactly what the class did. Without DI you'd be stuck with whatever the class new -ed internally.

The same Checkout class runs against the real Stripe gateway in production and a fake gateway in a test — purely by injecting a different implementation. You understand every line now.

In a real test project you'd often use a mocking library like Moq to generate the fake automatically, but a hand-written fake like FakeGateway works exactly the same way — and it only exists because the dependency was injectable.

Q: Do I always need a container to do dependency injection?

No. DI is just the pattern of passing dependencies in from outside — the first examples in this lesson wire everything by hand with no container at all. A container only automates that wiring once your graph gets big. The pattern is the important bit.

Q: What's the difference between DI and Inversion of Control?

Inversion of Control is the broad idea of handing control of object creation to something else. Dependency injection is one specific way of achieving it — supplying a class's dependencies through its constructor. A DI container is the tool that performs the inversion for you.

Default to Transient for stateless services. Use Scoped for things that should be shared within a single request but not across them (like a database context). Use Singleton only for genuinely shared, thread-safe state such as a cache or configuration.

Q: Why is the service-locator pattern considered bad?

Because pulling services from the container deep inside your classes hides their dependencies — you can't tell what a class needs by looking at its constructor, which makes it harder to test and reason about. Inject dependencies explicitly instead, and only touch the container at your app's composition root.

Because a class is given its dependencies, a test can hand it fakes instead of the real database or network service. You control the fake's behaviour and can inspect what the class did with it — fast, isolated, and repeatable.

No blanks this time — just a brief and an outline. The contracts and implementations are written for you; your job is to build the two dependencies in Main and inject them into an OrderService through its constructor (no container — wire it by hand). Run it and check your output against the expected lines in the comments.

Practice quiz

What does 'dependency injection' mean?

  • A class creates its own dependencies with new
  • A class is given its dependencies from outside instead of creating them
  • All dependencies are static fields
  • Dependencies are resolved with reflection at compile time

Answer: A class is given its dependencies from outside instead of creating them. DI means a class receives its collaborators from outside (e.g. via its constructor) rather than new-ing them itself.

Why depend on an interface instead of a concrete class?

  • It runs faster
  • It uses less memory
  • You can swap implementations without changing the dependent class
  • Interfaces are required by C#

Answer: You can swap implementations without changing the dependent class. Programming to an interface lets you substitute a different implementation (real, fake, upgraded) without editing the code that uses it.

How many instances does AddSingleton create for the whole application?

  • One per scope
  • A new one each resolve
  • One, shared for the entire app
  • One per thread

Answer: One, shared for the entire app. A Singleton is created once and shared for the entire application lifetime.

When is a Scoped service created?

  • Once per scope (e.g. one web request)
  • Once for the whole app
  • A fresh one every resolve
  • Never — it must be created manually

Answer: Once per scope (e.g. one web request). AddScoped gives one instance per scope; in a web app that scope is typically one HTTP request.

What does AddTransient do?

  • Shares one instance app-wide
  • Creates a brand-new instance every time it's resolved
  • Creates one per scope
  • Caches the instance for 5 minutes

Answer: Creates a brand-new instance every time it's resolved. A Transient service is built fresh every single time it's resolved — the safest default because it can't hold stale state.

What is the captive dependency trap?

  • Two services depending on each other
  • A Singleton holding a reference to a shorter-lived Scoped service
  • A service registered twice
  • Forgetting to call BuildServiceProvider

Answer: A Singleton holding a reference to a shorter-lived Scoped service. A Singleton that takes a Scoped service traps that scoped instance for the whole app lifetime — it's never refreshed per request.

A service may only depend on something with which lifetime?

  • A shorter lifetime
  • An equal or longer lifetime
  • Only the same lifetime
  • Any lifetime — it never matters

Answer: An equal or longer lifetime. The rule: a service may only depend on something with an equal or longer lifetime. Singleton → Scoped is the captive-dependency trap.

What's the difference between GetService<T>() and GetRequiredService<T>()?

  • They are identical
  • GetService throws if missing; GetRequiredService returns null
  • GetService returns null if missing; GetRequiredService throws
  • GetRequiredService is async

Answer: GetService returns null if missing; GetRequiredService throws. GetService<T>() returns null for an unregistered service, while GetRequiredService<T>() throws a clear exception — prefer the latter so failures are loud.

Which injection style does the lesson recommend?

  • Property injection
  • Method injection
  • Service-locator pattern
  • Constructor injection

Answer: Constructor injection. Constructor injection makes a class's required dependencies obvious and impossible to forget, so it's preferred over property/method injection.

Why is the service-locator pattern considered an anti-pattern?

  • It's slower than constructor injection
  • It hides a class's real dependencies, making it harder to test and reason about
  • It only works with singletons
  • It can't resolve interfaces

Answer: It hides a class's real dependencies, making it harder to test and reason about. Calling GetService deep inside business classes hides their dependencies, defeating the point of DI. Inject through the constructor instead.