IoC (Holywood Principle)

Inversion of Control (Hollywood Principle)

Inversion of Control (IoC) is a design principle where the flow of control is inverted: instead of your code creating and managing its dependencies, an external mechanism (a framework or container) coordinates object creation and composition. The name comes from the phrase Don't call us, we'll call you — your code defines what it needs, and the framework provides it. In practice, IoC is what makes ASP.NET Core's builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>() work: the framework reads the registrations, inspects constructor signatures, and wires everything together at request time. Without IoC, every class would new its own dependencies, creating a tightly coupled object graph that cannot be tested or reconfigured without editing source code.

IoC is the principle; Dependency Injection (DI) is the most common technique for implementing it. See Dependency Injection for the full treatment of DI patterns, service lifetimes, and ASP.NET Core's DI container.

How It Works

Without IoC, a class creates its own dependencies:

// Tightly coupled — OrderService controls its own dependencies
public class OrderService
{
    private readonly SqlOrderRepository _repo = new SqlOrderRepository("conn-string");
    private readonly SmtpEmailSender _email   = new SmtpEmailSender("smtp.example.com");
}

With IoC (via constructor injection), the container provides dependencies:

// Loosely coupled — dependencies are injected, not created
public class OrderService(IOrderRepository repo, IEmailSender email)
{
    // repo and email are provided by the DI container
}

// Registration in Program.cs
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();

The container calls OrderService with the right implementations — "don't call us, we'll call you."

IoC vs Dependency Inversion Principle (DIP)

These are related but distinct:

DIP is the why (depend on interfaces); IoC/DI is the how (let the container wire the concrete implementations). You can follow DIP without a DI container (manual wiring in Main), but a container makes it practical at scale.

Pitfalls

Service Locator Anti-Pattern

What goes wrong: instead of injecting dependencies, a class calls a global ServiceLocator.Get<IOrderRepository>() to resolve them. This is IoC in name only — the class still controls its own dependency resolution.

Why it matters: dependencies are hidden (not visible in the constructor), making the class hard to test (you must configure the global locator in tests) and hard to reason about (you can't tell what a class needs without reading its implementation).

Mitigation: always use constructor injection. The constructor signature is the contract — it makes dependencies explicit and testable.

Over-Injection (Constructor Bloat)

What goes wrong: a class has 8+ constructor parameters. Every new feature adds another dependency. The constructor becomes a sign that the class has too many responsibilities.

Why it happens: DI makes adding dependencies easy — too easy. Each dependency feels justified in isolation.

Mitigation: treat a constructor with more than 4-5 parameters as a design smell. Extract a new service or aggregate related dependencies into a parameter object. The root cause is usually an SRP violation.

Circular Dependencies

What goes wrong: ServiceA depends on ServiceB, which depends on ServiceA. The DI container throws at startup.

Why it happens: two services that should be decoupled have grown to depend on each other.

Mitigation: introduce an interface or event to break the cycle. If A needs to notify B, have A publish an event that B subscribes to — no direct dependency.

Tradeoffs

Decision Option A Option B When A When B
Constructor injection vs property injection Constructor injection: dependencies required at creation Property injection: dependencies set after creation Almost always — makes dependencies explicit, enforced at compile time, immutable Legacy code where constructors cannot be changed, or optional cross-cutting concerns like logging that have safe defaults
DI container vs manual wiring Framework DI container (IServiceCollection) Manual new in Main or composition root Any non-trivial application — container handles lifetime, scoping, and graph resolution Small CLI tools, scripts, or when the dependency graph is 3-5 objects and a container adds ceremony
Single container vs multiple containers One root IServiceProvider for the app Multiple containers or child scopes for modules Standard ASP.NET Core apps — one DI root with request scopes Plugin architectures, multi-tenant isolation, or test scenarios needing independent service graphs

Decision rule: use constructor injection via the built-in ASP.NET Core DI container as the default. Question the pattern only when constructors have more than 5 parameters (SRP violation signal) or when you are writing infrastructure that genuinely needs property injection.

Questions

References


Whats next