Your class needs a database service.
So you do this:
public class UserService
{
private readonly UserRepository _repository;
public UserService()
{
_repository = new UserRepository();
}
}
It works.
But now your UserService is responsible for creating the thing it depends on.
That small decision can make your application harder to test, maintain, and change.
This is where Dependency Injection (DI) comes in.
What Is Dependency Injection?
Dependency Injection is a design technique where a class receives the dependencies it needs from the outside instead of creating them itself.
Instead of this:
public class UserService
{
private readonly UserRepository _repository;
public UserService()
{
_repository = new UserRepository();
}
}
You inject the dependency:
public class UserService
{
private readonly UserRepository _repository;
public UserService(UserRepository repository)
{
_repository = repository;
}
}
Now UserService doesn't care how UserRepository is created.
It simply says:
"I need a UserRepository. Give me one."
That's the core idea behind dependency injection.
Why Does This Matter?
Consider a service that directly creates several dependencies:
public class OrderService
{
private readonly OrderRepository _repository;
private readonly EmailService _emailService;
private readonly PaymentService _paymentService;
public OrderService()
{
_repository = new OrderRepository();
_emailService = new EmailService();
_paymentService = new PaymentService();
}
}
Now OrderService is tightly coupled to the concrete implementations.
What happens when you want to:
- Replace the database?
- Use a different payment provider?
- Mock the email service during testing?
You have to modify the class.
With DI:
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IEmailService _emailService;
private readonly IPaymentService _paymentService;
public OrderService(
IOrderRepository repository,
IEmailService emailService,
IPaymentService paymentService)
{
_repository = repository;
_emailService = emailService;
_paymentService = paymentService;
}
}
Now the class depends on abstractions, and the actual implementations can be supplied externally.
Dependency Injection and Interfaces
DI becomes particularly useful when combined with interfaces.
For example:
public interface IEmailService
{
Task SendAsync(string email, string message);
}
You can have:
public class EmailService : IEmailService
{
public Task SendAsync(string email, string message)
{
// Send email
return Task.CompletedTask;
}
}
Your service doesn't need to know about EmailService.
It only needs IEmailService:
public class UserService
{
private readonly IEmailService _emailService;
public UserService(IEmailService emailService)
{
_emailService = emailService;
}
}
This makes the code more flexible and easier to test.
How Does ASP.NET Core Handle DI?
ASP.NET Core has a built-in Dependency Injection container.
You register your dependencies:
builder.Services.AddScoped<IEmailService, EmailService>();
Then ASP.NET Core can automatically provide the dependency:
public class UserService
{
private readonly IEmailService _emailService;
public UserService(IEmailService emailService)
{
_emailService = emailService;
}
}
You don't manually create EmailService with new.
The DI container handles it for you.
The Three Common Lifetimes
In ASP.NET Core, you'll commonly see:
Transient
builder.Services.AddTransient<IEmailService, EmailService>();
A new instance is created each time the dependency is requested.
Scoped
builder.Services.AddScoped<IUserService, UserService>();
One instance is created per request.
Singleton
builder.Services.AddSingleton<ICacheService, CacheService>();
One instance is created for the lifetime of the application.
Choosing the right lifetime matters because the wrong lifetime can lead to bugs, unnecessary object creation, or concurrency issues.
The Bigger Picture
Dependency Injection isn't about avoiding the new keyword everywhere.
The real goal is to separate object creation from object usage.
Your business logic should focus on what it needs to do, while another part of the application handles how its dependencies are created and provided.
That leads to code that is:
- Easier to test
- Easier to replace
- Less tightly coupled
- Easier to maintain
Key Takeaway
Dependency Injection is essentially this:
Don't make a class build the tools it needs. Give it the tools instead.
Once you understand that idea, DI containers, interfaces, service registration, and constructor injection in frameworks like ASP.NET Core become much easier to understand.
Top comments (0)