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
Makes classes loosely coupled
Improves testability (mock services easily)
Encourages cleaner architecture
Makes it easier to replace implementations
Built-in DI container in .NET reduces extra configuration
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
Transient → frequent, lightweight tasks
Scoped → web request-specific operations
Singleton → share the same instance safely
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
Registering the wrong lifetime (e.g., using Singleton for DbContext)
Injecting too many services into one class → violates Single Responsibility Principle
Using service locator instead of constructor injection
Forgetting to register an interface
Circular dependencies between services
Best Practices for Dependency Injection in .NET
Prefer constructor injection
Use interfaces for all services
Group service registrations in extension methods
Choose correct service lifetimes
Keep services small and single-purpose
Avoid building dependency graphs manually
Use the built-in DI container unless advanced features are needed
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.