What Happens Behind the Scenes with Dependency Injection in .NET Core
As a curious audience, we looked for a way to access the movie set.
You know how to inject services using interfaces in .NET Core. You’ve probably written code like this countless times:
public class ItemsController : ControllerBase
{
private readonly IItemService _itemService;
public ItemsController(IItemService itemService)
{
_itemService = itemService;
}
// Actions here...
}
And of course, you’ve registered your services in Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IItemRepository, ItemRepository>();
services.AddScoped<IItemService, ItemService>();
}
It works great, right? Your code is clean, and services are injected neatly into controllers or wherever you need them. But have you ever wondered what actually happens behind the scenes when you set up dependency injection (DI) like this? How does the framework know what to inject and when?
Embark on a journey of continuous learning and exploration with DotNet-FullStack-Dev. Uncover more by visiting our https://dotnet-fullstack-dev.blogspot.com reach out for further information.
Let’s dig into what happens at runtime with DI and how it relates to your Item API
, ItemService
, and ItemRepository
. We’ll peel back the layers and take a conversational, code-driven look at how .NET Core handles all this magic for you.
Step 1: The DI Container Gets Ready (The Startup Phase)
When you start your .NET Core app, one of the first things that happens is the creation of a dependency injection container. Think of it like a big box where all your service registrations (like ItemService
and ItemRepository
) are stored.
In Startup.cs
, the ConfigureServices
method registers services with specific lifetimes (like Scoped
, Transient
, or Singleton
). The framework now knows what classes are required and how long they should live:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IItemRepository, ItemRepository>();
services.AddScoped<IItemService, ItemService>();
}
Here’s what this registration means in human terms:
- AddScoped: “Dear DI container, every time there’s an HTTP request, please create one instance of
ItemService
andItemRepository
and share it across that request. But don’t keep it for the next request."
Step 2: The Controller Asks for Dependencies
Now, let’s say your app receives a request to the ItemsController
. The framework sees that your controller class requires an instance of IItemService
in its constructor:
public class ItemsController : ControllerBase
{
private readonly IItemService _itemService;
public ItemsController(IItemService itemService)
{
_itemService = itemService;
}
// Actions here...
}
This is where the DI container comes into play. The container looks at your ItemsController
and says, “Okay, I need to give this controller an IItemService
. Where can I get it?”
Since we registered IItemService
in Startup.cs
, the container knows it should provide an instance of ItemService
whenever IItemService
is requested.
Step 3: Resolving Dependencies (The Chain Reaction)
The DI container doesn’t just stop with IItemService
. The real magic happens when ItemService
itself has dependencies—such as IItemRepository
:
public class ItemService : IItemService
{
private readonly IItemRepository _itemRepository;
public ItemService(IItemRepository itemRepository)
{
_itemRepository = itemRepository;
}
// Service logic here...
}
Since ItemService
needs IItemRepository
, the container looks for how to resolve that dependency too. It sees the registration for IItemRepository
in Startup.cs
and provides an instance of ItemRepository
to ItemService
.
This chain reaction continues until all the dependencies are resolved. In this case:
- ItemsController needs
IItemService
, so the container providesItemService
. - ItemService needs
IItemRepository
, so the container providesItemRepository
.
All this happens automatically — no manual instantiation, no new
keyword required.
Step 4: Object Creation (Behind the Curtain)
When the container resolves all dependencies, it instantiates the objects as needed. Let’s look at what the container does behind the scenes for this example:
ItemRepository is created first because ItemService
depends on it.
var itemRepository = new ItemRepository();
ItemService is created next, with itemRepository
passed into its constructor.
var itemService = new ItemService(itemRepository);
Finally, ItemsController is created with itemService
injected into it.
var itemsController = new ItemsController(itemService);
Each service is created only when it’s needed and only once per HTTP request (thanks to AddScoped
).
Step 5: Clean Up After the Request
Once the HTTP request is completed, the scoped services are disposed of by the DI container. This cleanup process ensures that any resources (like database connections) are released properly, preventing memory leaks.
For AddScoped
, this means the instances of ItemService
and ItemRepository
are disposed of after the request ends, and fresh instances are created for the next request.
Have you asked yourself at least once like me, dependency-injection-is-a-curse-to-developers then have a look at my thoughts.
Why All This Matters for You as a Developer
Now that you know what happens under the hood with DI in .NET Core, you can see how much heavy lifting the framework does for you. Here’s why this is important:
- No manual object creation: You don’t have to worry about instantiating
ItemService
orItemRepository
. The DI container takes care of that. - Loose coupling: By injecting dependencies through interfaces (
IItemService
,IItemRepository
), your code is more flexible and easier to test. You can swap implementations without changing the rest of your code. - Automatic lifecycle management: Services are created and destroyed at the right times, which means you don’t have to worry about managing their lifecycles manually.
- Testability: You can mock dependencies in your unit tests. For example, when testing
ItemsController
, you can mockIItemService
to test only the controller’s behavior.
A Quick Look at Dependency Injection in Action
To sum it up, let’s walk through a typical flow when a user makes a request to fetch all items from your ItemsController
.
- Request comes in: A user hits the
/api/items
endpoint. - DI container kicks in: The container sees that
ItemsController
needs anIItemService
. - Chain reaction: The container resolves
IItemService
by creating an instance ofItemService
. It also resolvesIItemRepository
to pass intoItemService
. - Response goes out: The controller uses the service to fetch data from the repository, and the response is sent back to the user.
All of this happens without you having to manually instantiate objects. You focus on writing clean, modular code, and the framework handles the rest.
Wrapping It Up
Dependency Injection in .NET Core is more than just a pattern — it’s a built-in mechanism that simplifies your application’s structure and promotes good software design. By understanding what happens behind the scenes, you can appreciate how much work the DI container is doing for you at runtime.
Next time you’re registering services or injecting dependencies, think about that invisible chain reaction happening under the hood. The DI container is resolving dependencies, managing lifecycles, and making sure everything works smoothly — all while you get to write cleaner, more maintainable code.
If you’re already using DI, awesome! If not, now is the perfect time to start. It’s not just about convenience; it’s about making your application more flexible, testable, and future-proof.
Let me know your thoughts — what’s been your experience with DI? Have you run into any tricky situations or found any cool use cases? Let’s chat about it!