DEV Community

Ahmed Omeiza
Ahmed Omeiza

Posted on

Dependency Injection: Stop Letting Your Classes Build Their Own Dependencies

Your class needs a database service.

So you do this:

public class UserService
{
    private readonly UserRepository _repository;

    public UserService()
    {
        _repository = new UserRepository();
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

You inject the dependency:

public class UserService
{
    private readonly UserRepository _repository;

    public UserService(UserRepository repository)
    {
        _repository = repository;
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

You can have:

public class EmailService : IEmailService
{
    public Task SendAsync(string email, string message)
    {
        // Send email
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

Then ASP.NET Core can automatically provide the dependency:

public class UserService
{
    private readonly IEmailService _emailService;

    public UserService(IEmailService emailService)
    {
        _emailService = emailService;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

A new instance is created each time the dependency is requested.

Scoped

builder.Services.AddScoped<IUserService, UserService>();
Enter fullscreen mode Exit fullscreen mode

One instance is created per request.

Singleton

builder.Services.AddSingleton<ICacheService, CacheService>();
Enter fullscreen mode Exit fullscreen mode

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)