What is Dependency Injection?
Lets starts Dependency Injection with this real life example that we have to create a user service which send email when users register, the code would look like:
public class UserService
{
private readonly EmailService _emailService;
public UserService()
{
_emailService = new EmailService();
}
public void RegisterUser(string email)
{
// Save user to database
_emailService.SendWelcomeEmail(email);
}
}
At first glance, this code seems perfectly fine. The UserService creates an instance of EmailService and uses it to send a welcome email.
However, this approach introduces a significant problem.
The UserService is tightly coupled to EmailSerivce. So here the class UserService is responsible not only to register users but also it needs to creates its own dependency which is EmailService. Suppose initially EmailService implemented SMTP to send emails but later you decide to use SendGrid, you will have to modify UserService class also.
This violates one of the most important principles of software design: classes should focus on their own responsibilities rather than managing the creation of other objects.
This is where Dependency Injection (DI) comes in.
Dependency Injection Defined
Dependency Injection is a design pattern that allows a class to receive the objects it depends on from an external source instead of creating them itself.
Instead of constructing dependencies using the new keyword, they are injected into the class, usually through its constructor.
Let’s rewrite the previous example using Dependency Injection.
public class UserService
{
private readonly IEmailService _emailService;
public UserService(IEmailService emailService)
{
_emailService = emailService;
}
public void RegisterUser(string email)
{
// Save user to database
_emailService.SendWelcomeEmail(email);
}
}
Notice that the UserService no longer creates an EmailService. Instead, it simply declares that it needs an object implementing IEmailService.
Someone else is responsible for providing that dependency.
This simple change makes a huge difference.
Understanding Dependencies
A dependency is simply another object that a class needs to perform its work.
Consider this example:
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository repository,
IEmailService emailService,
ILogger<OrderService> logger)
{
_repository = repository;
_emailService = emailService;
_logger = logger;
}
public void PlaceOrder(Order order)
{
_repository.Save(order);
_emailService.SendOrderConfirmation(order.CustomerEmail);
_logger.LogInformation("Order placed successfully.");
}
}
The OrderService depends on three different services:
IOrderRepositoryfor storing orders.IEmailServicefor sending confirmation emails.ILoggerfor recording application logs.
Rather than creating these objects itself, it receives them through its constructor.
Before vs After Dependency Injection
Without Dependency Injection
OrderService
│
├── new OrderRepository()
├── new EmailService()
└── new Logger()
In this design:
- The class creates all of its dependencies.
- Replacing implementations requires modifying the class.
- Unit testing becomes difficult.
- The class is tightly coupled to specific implementations.
With Dependency Injection
OrderService
▲
│
+-----------------------------+
| ASP.NET Core DI Container |
+-----------------------------+
│
├── OrderRepository
├── EmailService
└── Logger
Now the OrderService simply requests the services it needs, while the ASP.NET Core Dependency Injection container creates and supplies the appropriate implementations.
Service Lifetimes
When registering services, you specify their lifetime.
There are three lifetimes.
1. Transient
builder.Services.AddTransient<IEmailService, EmailService>();
A new object is created every time it’s requested.
Best for:
- Lightweight services
- Stateless services
2. Scoped
builder.Services.AddScoped<IOrderService, OrderService>();
One instance per HTTP request.
Best for:
- Business services
- Repository pattern
- Entity Framework DbContext
Scoped is the most commonly used lifetime.
3. Singleton
builder.Services.AddSingleton<ICacheService, CacheService>();
Only one instance exists for the application’s lifetime.
Best for:
- Configuration
- Logging
- Caching
- Shared services
Avoid storing request-specific data in singleton services.
Benefits of Dependency Injection
Using Dependency Injection provides several important advantages:
1. Loose Coupling
Classes depend on abstractions (interfaces) rather than concrete implementations, making it easier to replace or extend functionality.
2. Easier Testing
Mock implementations can be injected during unit testing without modifying application code.
3. Better Maintainability
Business logic remains separate from object creation, resulting in cleaner and more organized code.
4. Improved Reusability
Services can be reused across multiple parts of an application without duplicating code.
5. Easier Refactoring
Changing an implementation often requires updating only the Dependency Injection configuration rather than every class that uses the service.
Best Practices
- Prefer constructor injection over property injection.
- Program against interfaces, not concrete classes.
- Keep services focused on a single responsibility.
- Use Scoped for most business services.
- Avoid injecting too many dependencies into one class.
- Never manually instantiate services that are managed by the DI container.
- Use Singleton only for thread-safe shared services.
Key Takeaway
Dependency Injection is not a framework feature exclusive to ASP.NET Core—it is a software design pattern that promotes loose coupling and maintainable code.
ASP.NET Core simply provides a powerful built-in Dependency Injection container that makes implementing this pattern straightforward.
