100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Dependency Injection Basics

Understand how dependency injection decouples classes from their collaborators, and how the built-in .NET DI container manages object lifetimes.

.NET Ecosystem & TestingIntermediate9 min readJul 9, 2026
Analogies

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.

csharp
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

Was this page helpful?

Topics covered

#CStudyNotes#Programming#DependencyInjectionBasics#Dependency#Injection#Registering#Resolving#StudyNotes#SkillVeris#ExamPrep