Dependency Injection Basics
Dependency Injection (DI) is a pattern where a class receives the objects it depends on from the outside, rather than constructing them itself with new. Instead of a OrderService internally creating a SqlOrderRepository, it declares a dependency on the IOrderRepository interface via its constructor, and something external — the caller, or an IoC (Inversion of Control) container — supplies the concrete implementation. This inverts control of object creation, hence the term, and it is the foundation of loosely coupled, testable, and configurable application architecture. .NET has first-class DI baked into its hosting model via Microsoft.Extensions.DependencyInjection, used throughout ASP.NET Core, Generic Host, and Worker Services.
Cricket analogy: A batting coach doesn't forge his own bat in a workshop — he declares a dependency on 'a bat meeting MRF specs' and the team's kit manager supplies the actual concrete bat, just like an OrderService depending on IOrderRepository.
Registering and Resolving Services
The built-in container works around an IServiceCollection where you register mappings from an abstraction to an implementation, and an IServiceProvider that resolves object graphs on demand, automatically injecting the constructor dependencies of whatever you ask for. Registration happens once at startup; resolution happens continuously as requests come in (in a web app) or as the application runs.
Cricket analogy: A team registers its full squad list before the tournament starts (IServiceCollection), then the team management resolves who actually walks out to bat for each specific match, request by request, throughout the series.
public interface IOrderRepository
{
Task<Order?> FindAsync(int id);
}
public sealed class SqlOrderRepository(AppDbContext db) : IOrderRepository
{
public Task<Order?> FindAsync(int id) =>
db.Orders.FirstOrDefaultAsync(o => o.Id == id);
}
public sealed class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
public async Task<Order> GetOrderAsync(int id)
{
var order = await repository.FindAsync(id)
?? throw new InvalidOperationException($"Order {id} not found");
logger.LogInformation("Loaded order {OrderId}", id);
return order;
}
}
// Startup / Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<OrderService>();
var app = builder.Build();Service Lifetimes
Every registration declares a lifetime that controls how often a new instance is created. Transient creates a brand-new instance every time it is requested — appropriate for lightweight, stateless services. Scoped creates one instance per logical scope, which in ASP.NET Core corresponds to a single HTTP request — appropriate for things like a DbContext that should be consistent across one request but not shared globally. Singleton creates exactly one instance for the entire application lifetime, shared by everyone — appropriate for expensive, thread-safe, stateless services like configuration readers or in-memory caches. Choosing the wrong lifetime is a very common source of bugs. Injecting a Scoped service into a Singleton (known as 'captive dependency') causes the scoped instance to be captured for the singleton's entire lifetime, defeating its per-request semantics — the container throws an InvalidOperationException in newer .NET versions when scope validation is enabled specifically to catch this mistake.
Cricket analogy: A Transient service is like a fresh throw-down ball for every net session — used once and gone; a Scoped service is like the same match ball for one innings; a Singleton is the one trophy shared by the whole tournament, and handing the trophy to a single practice net (a captive dependency) would be a mistake.
DI is not the same thing as an IoC container — dependency injection is the pattern (passing dependencies in), while the container is just a tool that automates wiring. You can practice DI by hand (manual constructor injection with no container at all), which is common in small libraries and unit tests.
Constructor vs. Other Injection Styles
Constructor injection is strongly preferred in idiomatic C#: dependencies are explicit, required, and immutable once set, and a class literally cannot be constructed in an invalid state. Property injection (setting a public property after construction) and method injection (passing a dependency only to the method that needs it) exist for optional dependencies or framework constraints, but they hide required dependencies and allow objects to exist in a partially-configured state, so they're used far less often in modern .NET code.
Cricket analogy: Constructor injection is like naming your opening pair before the toss — explicit and required; property injection is like swapping a batter in mid-over after play has already started, which leaves the team in an inconsistent, half-set lineup.
- DI inverts control of object creation: classes declare dependencies via constructor parameters instead of instantiating them directly.
- Register abstractions-to-implementations on IServiceCollection; the built-in IServiceProvider resolves the full object graph.
- Transient creates a new instance per request for the service; Scoped creates one per logical scope (e.g. HTTP request); Singleton creates exactly one for the app's lifetime.
- Injecting a Scoped service into a Singleton creates a captive dependency bug — modern .NET can validate scopes to catch this at startup.
- Constructor injection is preferred because it makes dependencies explicit, required, and immutable.
- DI is a pattern; an IoC container is just an optional tool that automates the wiring.
Practice what you learned
1. What does 'inversion of control' mean in the context of dependency injection?
2. Which service lifetime creates exactly one shared instance for the entire application?
3. What problem occurs when a Scoped service is injected into a Singleton?
4. Why is constructor injection generally preferred over property injection in modern C#?
5. In ASP.NET Core, a Scoped service instance typically corresponds to what unit of work?
Was this page helpful?
You May Also Like
Interfaces in C#
Understand interfaces as pure behavioral contracts that any type can implement, including default interface methods, explicit implementation, and multiple interface inheritance.
Unit Testing with xUnit
Learn how to write, organize, and run automated tests for C# code using xUnit, the most widely used testing framework in the modern .NET ecosystem.
Static Members
Learn how static fields, methods, and classes attach behavior and state to a type itself rather than to individual instances, and when that design choice makes sense.
C# Interview Questions
A curated set of common C# interview questions and precise answers covering value vs reference types, async/await, LINQ, and object-oriented design.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics