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.Services and 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 EmailService itself.
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 Notification class does not create EmailService.
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:
Notification depends on IMessageService, not directly on EmailService or SmsService.
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
Easier Unit Testing
You can pass mocks or fakes into constructors.
No need to hit real databases, APIs, or external systems.
Improved Maintainability
Flexibility and Extensibility
Encourages Good Architecture
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:
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):
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 an EmailService.
When someone asks for IOrderService, give OrderService, etc.
Services are then resolved by:
8. Service Lifetimes: Transient, Scoped, Singleton
Service lifetime defines how long the service instance lives.
The list three lifetimes:
Transient – new instance every time
Scoped – one instance per HTTP request
Singleton – one instance for the entire application
Let’s look at each in detail.
8.1 Transient
Definition: A new instance is created every time it’s requested.
builder.Services.AddTransient<IValidator, UserValidator>();
Use Transient for:
Pros
No shared state issues
Always fresh
Cons
8.2 Scoped
Definition: One instance is created per HTTP request.
builder.Services.AddScoped<IProductService, ProductService>();
Use Scoped for:
Pros:
8.3 Singleton
Definition: One instance is created for the entire application lifetime.
builder.Services.AddSingleton<ILogService, LogService>();
Use Singleton for:
Pros
Cons
9. The Captive Dependency Problem
One of the more advanced topics is the captive dependency problem.
This happens when a long-living service (like a Singleton) depends on a shorter-living service (like Scoped).
For example:
builder.Services.AddSingleton<ReportingService>();
builder.Services.AddScoped<IUnitOfWork, EfUnitOfWork>();
public class ReportingService
{
private readonly IUnitOfWork _uow;
public ReportingService(IUnitOfWork uow)
{
_uow = uow; // Scoped into Singleton
}
}
Here:
ReportingService is a Singleton.
It depends on a Scoped service IUnitOfWork.
ASP.NET Core will throw an error like:
“Cannot consume scoped service ‘X’ from singleton ‘Y’.”
This is a captive dependency and is considered a design smell.
Rule of thumb:
Singleton cannot depend on Scoped or Transient (unless carefully managed).
Scoped can depend on Transient.
Transient can depend on anything (but design still matters).
10. DI in Controllers (Typical Usage)
The classic controller example:
public class ProductsController : ControllerBase
{
private readonly IProductService _service;
public ProductsController(IProductService service) // usually Scoped
{
_service = service;
}
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var product = _service.GetById(id);
return Ok(product);
}
}
Key points
IProductService is typically registered as Scoped.
ASP.NET Core automatically injects it into the constructor.
You don’t call new ProductService() anywhere; DI handles it.
11. DI in Middleware
Middleware supports constructor injection, but scoped services should be injected via InvokeAsync.
Constructor Injection for Singleton-like Services
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogService _log;
public LoggingMiddleware(RequestDelegate next, ILogService log)
{
_next = next;
_log = log;
}
public async Task InvokeAsync(HttpContext context)
{
_log.Log("Request started");
await _next(context);
_log.Log("Request finished");
}
}
Here, ILogService It is likely Singleton and safe to inject in the constructor.
Method Injection for Scoped Services
For Scoped services, the suggestion is to use method parameters:
public async Task InvokeAsync(HttpContext context, IMyScopedService service)
{
// This works - service is resolved per request
await service.DoWorkAsync();
await _next(context);
}
ASP.NET Core resolves the scoped service per request and passes it to InvokeAsync.
12. Method Injection in Controllers
The [FromServices] An attribute allows you to inject dependencies directly into individual action methods.
[HttpGet("report")]
public IActionResult GetReport([FromServices] IReportService reportService)
{
var report = reportService.Generate();
return Ok(report);
}
Use this pattern when:
13. Multiple Implementations of the Same Interface
As your application grows, you might need multiple implementations for the same interface (e.g., Email, SMS, Push notification). You can register multiple implementations and resolve them appropriately.
A common approach in ASP.NET Core is:
Register multiple implementations
Inject IEnumerable<IMessageService>
Or create a factory that picks one based on a key
Example: Multiple IMessageService Implementations
public class SmsService : IMessageService
{
public void Send(string msg) { /* send SMS */ }
}
public class EmailService : IMessageService
{
public void Send(string msg) { /* send Email */ }
}
builder.Services.AddTransient<IMessageService, SmsService>();
builder.Services.AddTransient<IMessageService, EmailService>();
public class NotificationCenter
{
private readonly IEnumerable<IMessageService> _messageServices;
public NotificationCenter(IEnumerable<IMessageService> messageServices)
{
_messageServices = messageServices;
}
public void SendAll(string message)
{
foreach (var svc in _messageServices)
{
svc.Send(message);
}
}
}
Alternatively, you can create a factory that chooses by type (e.g., "sms", "email") using a dictionary strategy.
14. Best Practices for DI in ASP.NET Core
Depend on Interfaces, not Concrete Types
Use Constructor Injection for Required Dependencies
Keep constructors small and meaningful.
If more than 5–6 dependencies, consider refactoring that class.
Avoid the Service Locator Pattern
Choose Correct Lifetimes
Avoid Captive Dependency
Keep Services Stateless Where Possible
Register Services in a Single Place
For maintainability, use extension methods for complex modules:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddBilling(this IServiceCollection services)
{
services.AddScoped<IBillingService, BillingService>();
// other billing registrations
return services;
}
}
15. Common Mistakes with DI
Putting Business Logic in Controllers instead of Services.
Misusing lifetimes, especially by making everything a Singleton.
Using new inside services, breaking DI principles.
Injecting too many dependencies into one class (God classes).
Not handling disposal of IDisposable services outside the container’s control.
16. Interview-Focused Recap
If you’re preparing for interviews, here are some typical questions from this article:
What is Dependency Injection, and why is it important in ASP.NET Core?
What problem does DI solve?
Explain the Dependency Inversion Principle with an example.
What are the different types of DI (constructor, property, method)?
What is an IoC container, and how does ASP.NET Core use it?
How do you register services with different lifetimes?
When do you use Transient/Scoped/Singleton?
What is the captive dependency problem?
How does DI work in middleware?
How do you inject services into action methods using [FromServices]?
How do you handle multiple implementations of the same interface?
If you can confidently answer these, you have a strong grasp of DI in ASP.NET Core.