If you’ve worked with ASP.NET Core, you’ve probably used Dependency Injection (DI) without thinking much about it.
You register services in Program.cs, inject them into controllers, and move on.
But why does DI exist at all?
What problem is it solving?
Let’s answer that properly — with code.
1. The problem: tight coupling
A very common beginner pattern looks like this:
public class PostsController : ControllerBase
{
private readonly PostsService _service = new PostsService();
[HttpGet]
public IEnumerable<string> Get()
{
return _service.GetPosts();
}
}
public class PostsService
{
public IEnumerable<string> GetPosts()
{
return new[] { "Post A", "Post B" };
}
}
At first glance, this looks fine.
The code works. The API runs.
So what’s the issue?
2. Why this becomes a problem
❌ Change is expensive
If tomorrow:
-
PostsServiceneeds database access - or you want
PostsServiceNew - or you want a mock service for testing
You must edit the controller code.
That means:
touching multiple files
risking bugs
violating the open for extension, closed for modification principle
❌ Testing is painful
Because the controller creates the dependency itself, you cannot easily replace it.
You’re forced to:
- hit a real database
- call real APIs
- or write hacks to bypass logic
This is tight coupling.
3. The core idea of Dependency Injection (one line)
Don’t create dependencies inside a class. Ask for them.
That’s it.
That’s the whole idea.
Instead of saying:
“I will decide which service to use”
You say:
“Give me something that follows this contract”
4. The fix: abstraction + constructor injection
First, define a contract (interface):
public interface IPostsService
{
IEnumerable<string> GetPosts();
}
Then implement it:
public class PostsService : IPostsService
{
public IEnumerable<string> GetPosts()
{
return new[] { "Post A", "Post B" };
}
}
Now inject it into the controller:
[ApiController]
[Route("api/posts")]
public class PostsController : ControllerBase
{
private readonly IPostsService _service;
public PostsController(IPostsService service)
{
_service = service;
}
[HttpGet]
public IEnumerable<string> Get()
{
return _service.GetPosts();
}
}
What changed?
- The controller no longer knows which implementation it uses
- It only depends on an abstraction
This is where **decoupling **starts.
5. Let ASP.NET Core resolve the dependency
Now tell ASP.NET Core which implementation to use:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// One-line decision
builder.Services.AddScoped<IPostsService, PostsService>();
var app = builder.Build();
app.MapControllers();
app.Run();
If later you add another implementation:
public class PostsServiceNew : IPostsService
{
public IEnumerable<string> GetPosts()
{
return new[] { "Post X", "Post Y" };
}
}
You can switch it like this:
builder.Services.AddScoped<IPostsService, PostsServiceNew>();
No controller changes.
No ripple effects.
6. What the DI container actually does
Behind the scenes, ASP.NET Core:
- creates the controller
- sees it needs IPostsService
- looks in the container
- creates the correct implementation
- injects it automatically
You didn’t lose control — you moved the responsibility to the framework.
This is Inversion of Control (IoC).
7. How this relates to SOLID (briefly)
Abstraction
You program against interfaces, not implementations.
Decoupling
Controllers don’t know or care which service is used.
Dependency Inversion Principle (DIP)
High-level modules depend on abstractions, not concrete classes.
Inversion of Control (IoC)
The framework creates and wires objects for you.
DI is not a trend — it’s the practical application of these principles.
8. Why DI makes testing easy
Because dependencies are injected, you can pass a fake implementation:
public class FakePostsService : IPostsService
{
public IEnumerable<string> GetPosts()
{
return new[] { "Fake Post" };
}
}
var controller = new PostsController(new FakePostsService());
var result = controller.Get();
No database.
No infrastructure.
Just logic.
This is where DI really pays off.
9. When Dependency Injection is useful ✅
- You want replaceable implementations
- You care about testability
- You build medium to large applications
- You expect requirements to change
10. When DI is overkill ❌
- Very small scripts
- One-off console apps
- Code that will never grow or be tested
DI is a tool — not a rule.
Top comments (0)