Dependency Injection (DI) is at the heart of ASP.NET Core. You see it when you create controllers, register services, build middleware, or configure logging and configuration. Almost everything in ASP.NET Core is wired together using DI.
Yet many developers still only know it as:
“We add services in
builder.Servicesand use them in constructor.”
This article takes you beyond that.
We’ll start from the absolute basics and gradually move into real-world, interview-level, and advanced concepts—all in a clear, structured, and practical way.
1. What is Dependency Injection?
In simple terms:
Dependency Injection is a technique where a class receives the objects it needs (its dependencies) from the outside, instead of creating them itself.
A dependency is just another class or service that your class needs to do its work.
Example without DI (Tight Coupling)
public class EmailService
{
public void Send(string to, string message)
{
// send email logic
}
}
public class Notification
{
private readonly EmailService email = new EmailService(); // Bad
public void Notify(string userEmail)
{
email.Send(userEmail, "Welcome!");
}
}Here, Notification:
Creates
EmailServiceitself.It is tightly tied to
EmailService.Cannot easily switch to SMS, WhatsApp, or any other notification type.
Example with DI (Loose Coupling)
public class EmailService
{
public void Send(string to, string message)
{
// send email logic
}
}
public class Notification
{
private readonly EmailService email;
public Notification(EmailService email) // Injected
{
email = email;
}
public void Notify(string userEmail)
{
email.Send(userEmail, "Welcome!");
}
}Now:
The
Notificationclass does not createEmailService.It only uses the instance that is given to it.
This makes the class easier to test and easier to replace with other implementations.
2. What Problem Does DI Solve?
Dependency Injection primarily solves the problem of tight coupling.
When a class creates its own dependencies:
It is difficult to unit test (you can’t easily mock dependencies).
It is difficult to swap implementations (e.g., different email providers).
It is difficult to extend behavior (e.g., log every notification, or send SMS instead).
Tightly coupled classes are hard to test, hard to replace, and hard to extend.
With DI, instead:
You depend on abstractions (interfaces), not concrete classes.
You delegate the creation of dependencies to a central mechanism (IoC container).
You can swap implementations without modifying business logic.
3. DI and the Dependency Inversion Principle (SOLID)
DI is closely connected to the Dependency Inversion Principle (DIP)—the “D” in SOLID:
High-level modules should not depend on low-level modules. Both should depend on abstractions
Example with Interface Abstraction
public interface IMessageService
{
void Send(string msg);
}
public class EmailService : IMessageService
{
public void Send(string msg)
{
// send email
}
}
public class SmsService : IMessageService
{
public void Send(string msg)
{
// send sms
}
}
public class Notification
{
private readonly IMessageService messageService;
public Notification(IMessageService messageService)
{
messageService = messageService;
}
public void Notify(string message)
{
messageService.Send(message);
}
}Here:
Notificationdepends on IMessageService, not directly onEmailServiceorSmsService.You can inject any implementation of
IMessageService.You follow Dependency Inversion and make your code clean and flexible.
4. Benefits of Dependency Injection
Cleaner Code
Classes have single responsibilities.
No “new” scattered everywhere, creating dependencies.
Easier Unit Testing
You can pass mocks or fakes into constructors.
No need to hit real databases, APIs, or external systems.
Improved Maintainability
Changes in one service don’t force code changes everywhere.
Centralized configuration of dependencies.
Flexibility and Extensibility
Swap implementations (e.g., Email provider) by changing DI registration.
Encourages Good Architecture
Naturally pushes you toward SOLID, layered architecture, and clean code.
5. Types of Dependency Injection
Three main types of DI used in .NET:
Constructor Injection (most common)
Property Injection
Method Injection
5.1 Constructor Injection (Preferred)
Constructor injection is where dependencies are passed via the constructor.
public class PaymentService
{
private readonly ILogger _logger;
public PaymentService(ILogger logger)
{
_logger = logger;
}
public void Process()
{
_logger.Log("Payment processed.");
}
}Why it’s preferred:
Dependencies become required: the object cannot be constructed without them.
Makes the class easier to reason about.
Great for testability.
5.2 Property Injection
Dependencies are assigned via public properties (less common in ASP.NET Core, but still useful in some scenarios).
public class ReportService
{
public ILogger Logger { get; set; }
public void Generate()
{
Logger?.Log("Report generated.");
}
}Drawbacks
Dependencies are optional.
Object can be in an invalid state if properties are not set.
Not directly supported by the built-in container (you’d need manual wiring or custom logic).
5.3 Method Injection
Dependencies are passed directly into the method that needs them.
Example using [FromServices] to inject a service directly into an action method:
public IActionResult Run([FromServices] IReportService report)
{
return Ok(report.Generate());
}This is useful when:
You do not need the service in the entire class, only in one or two methods.
You want clear visibility that the method depends on an external service.
6. What is an IoC Container?
An IoC (Inversion of Control) Container is the mechanism that:
Knows which interfaces map to which implementations.
Creates objects when needed.
Resolves and injects dependencies automatically.
The IoC container is built in, so you don’t need third-party libraries to start.
Examples of IoC containers (in general):
Built-in ASP.NET Core container
Autofac
StructureMap
Ninject
SimpleInjector
In ASP.NET Core, we typically use the built-in container unless we have very advanced needs.
7. Registering Services in ASP.NET Core
In ASP.NET Core, DI registration is done in Program.cs.
builder.Services.AddTransient<IMessageService, EmailService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ILogService, LogService>();This tells the IoC container:
When someone asks for
IMessageService, give anEmailService.When someone asks for
IOrderService, giveOrderService, etc.

Join the conversation! Your thoughts help the community grow.