The dependency injection is a technique by which an object receives the collaborators it needs from the outside. Constructor injection is its most common form, but it does not require using a container.
It is important to distinguish it from IoC and DIP. They are related ideas, but they do not mean the same thing.
The Problem: “I Cook It, I Eat It”
Imagine a UserService class that needs to save data. A tightly coupled implementation might do this:
// ❌ BAD: High Coupling
public class UserService
{
private FileLogger _logger;
private SqlDatabase _database;
public UserService()
{
// THE ORIGINAL SIN: Using 'new' inside the class
_logger = new FileLogger("log.txt");
_database = new SqlDatabase("connectionString");
}
public void Register()
{
_logger.Log("Registering...");
_database.Save();
}
}Why is this terrible?
- Rigidity:
UserServiceis forever married toFileLoggerandSqlDatabase. If you want to change logging to the console or the database to the cloud, you have to modify this class. - Difficult to test in isolation: when instantiating
UserService, it tries to connect to the real database and write to disk.
The Solution: “Don’t Call Me, I’ll Call You”
Dependency Injection tells us: A class should not create its dependencies. It should request them.
Instead of manufacturing the Logger inside, we tell the world: “Hey, I need a Logger to work. Give it to me.”
// ✅ GOOD: Dependency Injection
public class UserService
{
private ILogger _logger;
private IDatabase _database;
// Constructor Injection: "Give me what I need"
public UserService(ILogger logger, IDatabase database)
{
_logger = logger;
_database = database;
}
public void Register()
{
_logger.Log("Registering...");
_database.Save();
}
}Now UserService doesn’t know what logger it uses. Is it a file? Is it the console? Is it a fake mock for tests? It doesn’t care. It only knows it implements ILogger.
Untangling the Alphabet Soup: DI vs DIP vs IoC
This is where everyone gets confused. Let’s distinguish the three key terms:
It is the principle (The ‘D’ of SOLID). It’s a theoretical rule.
It says: “High-level modules should not depend on low-level modules. Both should depend on abstractions.”
In the example: UserService should not depend on FileLogger, but on ILogger.
It is the general concept. It says: Invert the flow of control. Instead of your class controlling the creation of objects, something external (the framework, the main) controls it.
It is the concrete TECHNIQUE. It says: “The way to achieve IoC and comply with DIP is by passing dependencies through the constructor (or properties).”
Summary in one sentence: We use the Dependency Injection technique to apply the Dependency Inversion principle.
Types of Injection
There are mainly three ways to inject dependencies:
Constructor Injection (Recommended)
This is what we’ve seen. Dependencies are declared in the constructor.
- Advantage: forces providing dependencies when creating the object. It is advisable to validate them in the constructor to reject null values.
- Usage: In 99% of cases.
Property Injection (Setter Injection)
public class Client
{
public ILogger Logger { get; set; } // Optional
}- Advantage: Allows optional dependencies.
- Disadvantage: You might forget to set it and get a
NullReferenceExceptionat runtime.
Method Injection
Pass the dependency only to the method that needs it.
public void GenerateReport(IPDFGenerator generator) { ... }Who Creates the Objects Then? (DI Containers)
If UserService doesn’t create the Logger, who does? Someone has to do the new.
In a small application, we do it in the “Entry Point” (Main):
// Pure DI (Manual Injection)
void Main()
{
ILogger logger = new ConsoleLogger();
IDatabase db = new MySqlDatabase();
// We act as the "glue"
var service = new UserService(logger, db);
service.Register();
}But in large applications (like ASP.NET Core), we use a Dependency Injection Container (DI Container). The container is an intelligent list where we register our classes and it takes care of resolving dependencies automatically.
// Conceptual example in .NET Core
builder.Services.AddTransient<ILogger, ConsoleLogger>();
builder.Services.AddTransient<IDatabase, MySqlDatabase>();
builder.Services.AddTransient<UserService>();
// ... later ...
// The container sees that UserService needs ILogger and IDatabase,
// looks them up, creates them, and returns the assembled service.
var service = container.GetService<UserService>();The Power of Testing
The greatest immediate advantage of DI is the ease of performing unit tests.
Since UserService requests interfaces, in our tests we can pass Mocks (fake objects) to it.
[Test]
public void Test_Register_CallsSave()
{
// Arrange: Create fake dependencies
var mockLogger = new Mock<ILogger>();
var mockDb = new Mock<IDatabase>();
var service = new UserService(mockLogger.Object, mockDb.Object);
// Act
service.Register();
// Assert: Verify that Save() was called without touching a real database
mockDb.Verify(db => db.Save(), Times.Once);
}Dependency Injection is not a fad; it is a necessity for any professional software. It allows us to write code that is:
- Decoupled: Parts are interchangeable.
- Maintainable: Changing an implementation does not break the system.
- Testable: We can isolate each class to test it.