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.

DotNet Full Stack Dev
5 min readOct 7, 2024

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 and ItemRepository 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:

  1. ItemsController needs IItemService, so the container provides ItemService.
  2. ItemService needs IItemRepository, so the container provides ItemRepository.

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:

  1. No manual object creation: You don’t have to worry about instantiating ItemService or ItemRepository. The DI container takes care of that.
  2. 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.
  3. 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.
  4. Testability: You can mock dependencies in your unit tests. For example, when testing ItemsController, you can mock IItemService 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.

  1. Request comes in: A user hits the /api/items endpoint.
  2. DI container kicks in: The container sees that ItemsController needs an IItemService.
  3. Chain reaction: The container resolves IItemService by creating an instance of ItemService. It also resolves IItemRepository to pass into ItemService.
  4. 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!

--

--

DotNet Full Stack Dev
DotNet Full Stack Dev

Written by DotNet Full Stack Dev

Join me to master .NET Full Stack Development & boost your skills by 1% daily with insights, examples, and techniques! https://dotnet-fullstack-dev.blogspot.com

Responses (1)