Introduction

Dependency Injection (DI) is one of the most important patterns in modern .NET development. It helps you write cleaner, maintainable, testable, and scalable applications by removing tight coupling between classes. Instead of a class creating its own dependencies, DI allows you to “inject” those dependencies from the outside. This approach improves flexibility and makes your code easier to test and replace. In this article, we’ll break down DI in simple words and show you the correct way to implement it in .NET using practical examples.

What Is Dependency Injection?

Dependency Injection means giving an object the things it needs (dependencies) from the outside instead of letting it create them.

Bad Approach (Tightly Coupled)

public class OrderService {
    private readonly EmailService _email;

    public OrderService() {
        _email = new EmailService();
    }
}

Here, OrderService creates its own dependency, making it hard to test or replace.

Good Approach (Using DI)

public class OrderService {
    private readonly IEmailService _email;

    public OrderService(IEmailService email) {
        _email = email;
    }
}

The dependency is injected—much cleaner and more flexible.

Why Dependency Injection Is Important

DI is a core part of Clean Architecture, Onion Architecture, and Domain-Driven Design.

Registering Services in .NET

In .NET, DI is configured in Program.cs using the built-in service container.

Example Registration

builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ILoggerService, LoggerService>();

Each registration defines:

Understanding Service Lifetimes

Choosing the correct service lifetime is important.

1. Transient

A new instance is created every time it is requested.

services.AddTransient<IEmailService, EmailService>();

Use when: lightweight, stateless services.

2. Scoped

Created once per HTTP request.

services.AddScoped<IOrderService, OrderService>();

Use when: services dealing with per-request operations such as database operations.

3. Singleton

Created only once for the entire application.

services.AddSingleton<ILoggerService, LoggerService>();

Use when: shared data, configuration loaders, loggers.

Choosing the Right Lifetime

Using Constructor Injection (Best Practice)

Constructor injection is the most common and recommended way to inject dependencies.

Example

public class OrderController : ControllerBase {
    private readonly IOrderService _orderService;

    public OrderController(IOrderService orderService) {
        _orderService = orderService;
    }

    [HttpGet]
    public IActionResult GetOrders() => Ok(_orderService.GetOrders());
}

Why Constructor Injection Is Best

Using Interface-Based Design

Always create interfaces for services.

Example

public interface IEmailService {
    void Send(string to, string message);
}

public class EmailService : IEmailService {
    public void Send(string to, string message) {}
}

Benefits

Avoiding Service Locator Anti-pattern

Wrong Approach

var emailService = HttpContext.RequestServices.GetService<IEmailService>();

This is called Service Locator and should be avoided because:

Always inject dependencies instead.

Registering Generic Services

You can register generic types too.

Example

services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

Useful in repository patterns.

Using DI in Background Services

Hosted services also support DI.

Example

public class EmailBackgroundService : BackgroundService {
    private readonly IEmailService _emailService;

    public EmailBackgroundService(IEmailService emailService) {
        _emailService = emailService;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken) {
        // Logic here
        return Task.CompletedTask;
    }
}

Register it

services.AddHostedService<EmailBackgroundService>();

Using Options Pattern with DI

Configuration settings should also be injected.

Example

services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));

Inject it

public class EmailService : IEmailService {
    private readonly EmailSettings _settings;

    public EmailService(IOptions<EmailSettings> options) {
        _settings = options.Value;
    }
}

Common Mistakes to Avoid

Best Practices for Dependency Injection in .NET

Conclusion

Dependency Injection is one of the most powerful tools for building scalable and maintainable .NET applications. By understanding how DI works, choosing the right lifetimes, using constructor injection, and avoiding common pitfalls, you can write clean, testable, and well-structured code. Implementing DI the right way leads to better architecture, easier unit testing, and long-term maintainability for your .NET projects.