Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Pure DI vs. DI Containers: Understanding Dependency Wiring

Recommended resources

Hello! In our previous lesson, we established a crucial architectural principle: the Composition Root. This is the single, designated place in your application—like Application.OnStartup in WPF—where the entire object graph is assembled. This isolates the messy business of object creation from the clean, functional logic of your application.

Now that we know where this happens, we need to explore how. This lesson will compare the two primary methods for constructing the object graph within the Composition Root. First, we'll examine Pure DI, the practice of wiring up dependencies manually. Then, we'll see how a DI Container automates this process, saving you from writing a lot of boilerplate code. This directly addresses your goal of understanding the "why" behind these tools, not just the "how."

Pure DI: The Manual Approach

Pure DI (also called "Poor Man's DI") is Dependency Injection in its most fundamental form. It means you write the code in your Composition Root to manually create each service instance and pass it into the constructor of the object that needs it, using the new keyword. There are no third-party libraries or "magic" frameworks involved.

Imagine the Commerce class from a hypothetical e-commerce application. It needs several dependencies to do its job: an IBillingProcessor, an ICustomer repository, an INotifier, and an ILogger.

With Pure DI, your Composition Root would look something like this:

// Inside the Composition Root (e.g., Application.OnStartup in WPF)

// 1. Manually create all the low-level dependencies.
var logger = new FileLogger("log.txt");
var notifier = new EmailNotifier("smtp.server.com");
var customerRepository = new SqlCustomerRepository("connection_string");
var billingProcessor = new StripeBillingProcessor("api_key");

// 2. Manually "new up" the top-level service, injecting the dependencies.
var commerceService = new Commerce(
    billingProcessor, 
    customerRepository, 
    notifier, 
    logger);

// 3. Now the application can use the fully constructed 'commerceService'.
// For example, passing it to the main window's ViewModel.
var mainViewModel = new MainViewModel(commerceService);

This is the "dirty" work we mentioned in the last lesson. The Composition Root knows about every concrete type (FileLogger, SqlCustomerRepository, etc.). The rest of the application, like the Commerce class itself, remains blissfully ignorant, depending only on abstractions.

The main purpose of understanding Pure DI is that it lays bare the mechanics of dependency injection. There is no abstraction over the process of composing the object graph. It's just code. This approach is perfectly viable for smaller applications where the dependency graph is simple. However, as your application grows, this manual wiring becomes tedious and error-prone. If SqlCustomerRepository suddenly needs a new dependency, you have to go back and manually update its instantiation in the Composition Root.

This is the exact pain point that DI containers are designed to solve.


DI Containers: Automating the Graph Construction

A DI Container (also known as an Inversion of Control or IoC container) is a tool that automates the process of object creation and dependency wiring. Instead of manually writing new for every object, you delegate that responsibility to the container.

The container's job can be broken down into two main phases:

  1. Registration: You teach the container which concrete class to use when an interface is requested. For example, you "register" that any time a component asks for ILogger, the container should provide an instance of ConsoleLogger.
  2. Resolution: You ask the container for a single, top-level object (like our Commerce service). The container then inspects its constructor, sees it needs an IBillingProcessor, an ICustomer, etc., and recursively resolves the entire dependency graph for you.

The following article provides an excellent walkthrough of this concept. It first shows a decoupled example similar to our Pure DI discussion, then introduces what a container does.

Understanding Dependency Injection and Those Pesky Containers

This article from Code Magazine clearly separates the act of decoupling from the act of automating with a container.

First, quickly review the section A Decoupled Example to see how manual wiring works. Then, read the section How a DI Container Works carefully. It perfectly explains the core concepts of registration and resolution.

As the article explains, the container traverses the entire dependency graph. When you ask for Commerce, it sees the constructor public Commerce(IBillingProcessor processor, ...) and thinks:

  • "Okay, to build a Commerce, I need an IBillingProcessor."
  • It checks its registrations: "Ah, IBillingProcessor is mapped to StripeBillingProcessor."
  • It looks at StripeBillingProcessor's constructor. "Does it have dependencies?"
  • ...and so on, until it has built the entire tree of objects from the bottom up.

This process is visualized in the diagram below, showing the four stages a container manages. Today, we've focused on the first two: telling the container what to build (Registration) and having it build the graph (Object Creation and Injection).

This diagram illustrates the lifecycle of dependency management within a DI container, starting with registration and moving through creation, scope management, and finally, destruction.

To see this in action with the built-in .NET container that you're interested in, the following video provides a concise and practical demonstration.

Dependency injection fundamentals in C# - DI vs IoC vs DIP

This video by Amichai Mantinband gives a great hands-on look at the Microsoft.Extensions.DependencyInjection container.

Watch the segment from 10:09 to 12:17. Pay close attention to how he uses ServiceCollection to register the I GitHubClient to the GitHubClient, and then uses the ServiceProvider to resolve the GitHubService, automatically wiring up the dependency.

This automation is the primary purpose of a DI container. It removes the need for the large, brittle block of manual creation code required by Pure DI, making your Composition Root much cleaner and more maintainable, especially in large projects.


Pattern vs. Tool: A Crucial Distinction

It's vital to separate the design pattern from the tool.

  • Dependency Injection (DI) is the pattern of supplying dependencies to an object from an external source. You are applying this pattern whether you use Pure DI or a container.
  • A DI Container is a tool that helps you implement the pattern.

You can absolutely practice DI without a container. A container just makes it much easier at scale. This article makes the distinction very clearly.

Dependency Injection and DI Containers Are Not The Same Thing

This article directly addresses the common confusion between the DI pattern and DI containers.

Read the introduction and the "Key Pointers" for both Dependency Injection and DI Containers. Then, review the list under the heading Key Difference.


Conclusion

In this lesson, we've moved from the "where" of object creation to the "how." By understanding both the manual and automated approaches, you're better equipped to see that a DI container isn't magic—it's an automation of a clear and simple pattern.

Here are the key takeaways:

  • Pure DI is the manual process of instantiating and connecting objects within the Composition Root. It's fundamental and requires no external libraries, but becomes cumbersome in large applications.
  • A DI Container is a tool that automates the construction of the object graph through two main actions: Registration (mapping abstractions to concretions) and Resolution (building an object and all its dependencies on-demand).
  • Dependency Injection is the design pattern, while a DI container is a tool that makes implementing that pattern more efficient.

Now that we've seen how a container can resolve our dependencies, it's important to discuss how not to use it. In our next lesson, we will distinguish between proper Dependency Injection and the Service Locator anti-pattern, a common pitfall when first using DI containers.