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.
Services are then resolved by:
Constructors in controllers, services, middleware, etc.
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:
Lightweight, stateless services
Validation services
Simple helper utilities
Pros
No shared state issues
Always fresh
Cons
It can be expensive if the object creation is heavy and used in tight loops
8.2 Scoped
Definition: One instance is created per HTTP request.
builder.Services.AddScoped<IProductService, ProductService>();Use Scoped for:
Request-specific services
Database contexts
Anything that should share state during a single HTTP request but not across requests
Pros:
Good balance between sharing and isolation
Avoids concurrency problems across requests
8.3 Singleton
Definition: One instance is created for the entire application lifetime.
builder.Services.AddSingleton<ILogService, LogService>();Use Singleton for:
Caching services
Configuration readers
Lightweight, stateless services that are safe to share
Pros
Very efficient for repeated use
Good when the state must be shared globally
Cons
Must be thread-safe
Dangerous if used with objects that depend on request-specific data
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:
ReportingServiceis 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
IProductServiceis 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:
The service is only needed for one or a few actions.
You don’t want it as a required constructor dependency.
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
Improves testability and flexibility.
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
Don’t inject
IServiceProviderand manually resolve dependencies everywhere.Let DI inject what you need.
Choose Correct Lifetimes
DbContext → Scoped
Loggers → Singleton
Validation Services → Transient
Avoid Captive Dependency
Don’t allow Singleton to depend on Scoped.
Keep Services Stateless Where Possible
Easier to reason about and share (Singleton/Transient).
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
newinside 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.

Join the conversation! Your thoughts help the community grow.