Dependency Injection (DI) means you give an object the things it depends on, instead of letting it create them itself.
Inversion of Control (IoC) means object creation and wiring are moved out of your app code into another mechanism—often an IoC container like Microsoft.Extensions.DependencyInjection.
Start with an abstraction:
public interface IEngine
{
void Start();
}Two implementations:
public class Engine : IEngine
{
public void Start()
{
Console.WriteLine("Standard engine started.");
}
}public class Car
{
private readonly IEngine _engine;
public Car(IEngine engine)
{
_engine = engine;
}
public void Drive()
{
_engine.Start();
Console.WriteLine("Car is driving.");
}
}Car depends on IEngine, not on Engine directly.
That is the core DI idea:
Cardoes not donew Engine()- someone else provides the engine
IEngine engine = new Engine();
Car car = new Car(engine);
car.Drive();This is manual dependency injection.
If Car looked like this:
public class Car
{
private readonly Engine _engine = new Engine();
public void Drive()
{
_engine.Start();
Console.WriteLine("Car is driving.");
}
}then:
Caris tightly coupled toEngine- hard to replace with another engine
- harder to test
With DI:
- you can swap implementations
Carbecomes easier to test- responsibilities are cleaner
public class ElectricEngine : IEngine
{
public void Start()
{
Console.WriteLine("Electric engine powered on silently.");
}
}public class DieselEngine : IEngine
{
public void Start()
{
Console.WriteLine("Diesel engine started with a rumble.");
}
}Now the same Car can use any of them:
Car electricCar = new Car(new ElectricEngine());
electricCar.Drive();
Car dieselCar = new Car(new DieselEngine());
dieselCar.Drive();This shows the benefit clearly: the car code does not change.
Suppose a CarShow needs a Car.
public class CarShow
{
private readonly Car _car;
public CarShow(Car car)
{
_car = car;
}
public void Present()
{
Console.WriteLine("Welcome to the car show!");
_car.Drive();
}
}Manual wiring now becomes:
IEngine engine = new ElectricEngine();
Car car = new Car(engine);
CarShow show = new CarShow(car);
show.Present();Still fine for a small app.
But as the app grows:
CarShowdepends onCarCardepends onIEngine- other classes depend on other services
Then manual wiring becomes repetitive and messy.
That is where an IoC container helps.
An IoC container:
- knows how to create objects
- knows what dependencies they need
- resolves them automatically
In .NET, the common built-in container is:
Microsoft.Extensions.DependencyInjection
So instead of this:
var show = new CarShow(new Car(new ElectricEngine()));you register services once, and ask the container for CarShow.
Install package if needed:
dotnet add package Microsoft.Extensions.DependencyInjectionThen:
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddTransient<IEngine, Engine>();
services.AddTransient<Car>();
services.AddTransient<CarShow>();
var provider = services.BuildServiceProvider();
var show = provider.GetRequiredService<CarShow>();
show.Present();- container sees
CarShowneedsCar - container sees
CarneedsIEngine - container sees
IEnginemaps toEngine - it builds the whole object graph for you
This is IoC via container.
A simple way to distinguish them:
- Dependency Injection = the technique
- IoC container = a tool that automates the technique
So:
- manual constructor injection = DI
ServiceCollectionresolving dependencies = DI with an IoC container
Now suppose you register both:
services.AddTransient<IEngine, ElectricEngine>();
services.AddTransient<IEngine, DieselEngine>();This creates an important question:
Which one should Car get?
By default, if Car asks for a single IEngine, the built-in container generally resolves the last registered one for single-service resolution.
That can be surprising and unclear.
So when you have multiple implementations, you usually choose one of these patterns:
- inject
IEnumerable<IEngine> - use a factory
- use keyed/named resolution patterns
- create separate abstractions if they are meaningfully different
Example CarShow wants to display all engine types:
public class CarShow
{
private readonly IEnumerable<IEngine> _engines;
public CarShow(IEnumerable<IEngine> engines)
{
_engines = engines;
}
public void Present()
{
Console.WriteLine("Available engines at the car show:");
foreach (var engine in _engines)
{
engine.Start();
}
}
}Registration:
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddTransient<IEngine, ElectricEngine>();
services.AddTransient<IEngine, DieselEngine>();
services.AddTransient<CarShow>();
var provider = services.BuildServiceProvider();
var show = provider.GetRequiredService<CarShow>();
show.Present();This is good when you truly want all implementations.
If one Car should use one chosen engine, a factory is cleaner.
public class Car
{
private readonly IEngine _engine;
public Car(IEngine engine)
{
_engine = engine;
}
public void Drive()
{
_engine.Start();
Console.WriteLine("Car is driving.");
}
}using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddTransient<ElectricEngine>();
services.AddTransient<DieselEngine>();
services.AddTransient<Car>(sp =>
{
// choose which engine to use
var engine = sp.GetRequiredService<ElectricEngine>();
return new Car(engine);
});
services.AddTransient<CarShow>();
var provider = services.BuildServiceProvider();
var show = provider.GetRequiredService<CarShow>();
show.Present();CarShow:
public class CarShow
{
private readonly Car _car;
public CarShow(Car car)
{
_car = car;
}
public void Present()
{
Console.WriteLine("Welcome to the car show!");
_car.Drive();
}
}This is often the most straightforward way when a class needs one specific implementation.
If engine choice depends on runtime input, make a factory interface.
public interface IEngineFactory
{
IEngine Create(string engineType);
}public class EngineFactory : IEngineFactory
{
public IEngine Create(string engineType)
{
return engineType switch
{
"electric" => new ElectricEngine(),
"diesel" => new DieselEngine(),
_ => throw new ArgumentException("Unknown engine type")
};
}
}Then:
public class CarShow
{
private readonly IEngineFactory _engineFactory;
public CarShow(IEngineFactory engineFactory)
{
_engineFactory = engineFactory;
}
public void Present(string engineType)
{
var car = new Car(_engineFactory.Create(engineType));
Console.WriteLine($"Presenting a car with {engineType} engine:");
car.Drive();
}
}Registration:
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddSingleton<IEngineFactory, EngineFactory>();
services.AddTransient<CarShow>();
var provider = services.BuildServiceProvider();
var show = provider.GetRequiredService<CarShow>();
show.Present("electric");
show.Present("diesel");This works, though note that this particular factory manually creates engines with new. In bigger apps, you usually let the container help inside the factory too.
public interface IEngineFactory
{
IEngine Create(string engineType);
}using Microsoft.Extensions.DependencyInjection;
public class EngineFactory : IEngineFactory
{
private readonly IServiceProvider _serviceProvider;
public EngineFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IEngine Create(string engineType)
{
return engineType switch
{
"electric" => _serviceProvider.GetRequiredService<ElectricEngine>(),
"diesel" => _serviceProvider.GetRequiredService<DieselEngine>(),
_ => throw new ArgumentException("Unknown engine type")
};
}
}Registration:
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddTransient<ElectricEngine>();
services.AddTransient<DieselEngine>();
services.AddSingleton<IEngineFactory, EngineFactory>();
services.AddTransient<CarShow>();
var provider = services.BuildServiceProvider();This streamlines resolution when there are several dependencies.
If you're using newer .NET versions, Microsoft.Extensions.DependencyInjection supports keyed services, which is a strong fit for “ElectricEngine vs DieselEngine”.
Registration example:
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddKeyedTransient<IEngine, ElectricEngine>("electric");
services.AddKeyedTransient<IEngine, DieselEngine>("diesel");
services.AddTransient<CarShow>();
var provider = services.BuildServiceProvider();Resolve by key:
using Microsoft.Extensions.DependencyInjection;
public class CarShow
{
private readonly IServiceProvider _serviceProvider;
public CarShow(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Present(string engineType)
{
var engine = _serviceProvider.GetRequiredKeyedService<IEngine>(engineType);
var car = new Car(engine);
Console.WriteLine($"Presenting a car with {engineType} engine:");
car.Drive();
}
}Usage:
var show = provider.GetRequiredService<CarShow>();
show.Present("electric");
show.Present("diesel");This is often more maintainable than relying on registration order.
Car says:
I will create my own engine.
Car says:
I need an
IEngine. Please give me one.
The container says:
I know how to build
Car,CarShow, and the rightIEngine.
Use:
- constructor injection as the default
- interfaces like
IEngineto decouple implementations - manual DI for very small examples/apps
- Microsoft.Extensions.DependencyInjection when wiring grows
- factory or keyed services when multiple implementations exist
Avoid:
- creating dependencies directly with
newinside business classes - depending on concrete classes unless there's a good reason
- relying on “last registration wins” when multiple implementations exist
public interface IEngine
{
void Start();
}public class ElectricEngine : IEngine
{
public void Start() => Console.WriteLine("Electric engine started.");
}public class DieselEngine : IEngine
{
public void Start() => Console.WriteLine("Diesel engine started.");
}public class Car
{
private readonly IEngine _engine;
public Car(IEngine engine)
{
_engine = engine;
}
public void Drive()
{
_engine.Start();
Console.WriteLine("Car is driving.");
}
}using Microsoft.Extensions.DependencyInjection;
public class CarShow
{
private readonly IServiceProvider _serviceProvider;
public CarShow(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Present(string engineType)
{
var engine = _serviceProvider.GetRequiredKeyedService<IEngine>(engineType);
var car = new Car(engine);
car.Drive();
}
}using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddKeyedTransient<IEngine, ElectricEngine>("electric");
services.AddKeyedTransient<IEngine, DieselEngine>("diesel");
services.AddTransient<CarShow>();
var provider = services.BuildServiceProvider();
var show = provider.GetRequiredService<CarShow>();
show.Present("electric");
show.Present("diesel");- Dependency Injection is passing dependencies into classes.
- IoC container is the framework that automatically creates and passes those dependencies for you.
If you want, I can also rewrite this as:
- a beginner-friendly explanation, or
- an interview-style answer, or
- a full compilable console app.