Dependency Injection (DI) is a core concept in .NET since .NET Core, and it's the default pattern used in ASP.NET Core apps, console apps, and even workers or background services.
Whether you're building a web API or a desktop tool, mastering DI leads to cleaner, testable, and maintainable code.
1. What is Dependency Injection?
Dependency Injection is a design pattern where.
- A class doesn't create its dependencies.
- Instead, they are "injected" from outside (via constructors or setters).
This follows Inversion of Control (IoC), where the control of creating objects is inverted and handled by a container.
Without DI
public class ReportService
{
private readonly EmailSender _emailSender = new EmailSender(); // tightly coupled
}
With DI
public class ReportService
{
private readonly IEmailSender _emailSender;
public ReportService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
}
2. Setting Up Dependency Injection in .NET
DI in .NET is handled by the built-in service container via IServiceCollection.
In a .NET Core or .NET 6+ app, services are registered in Program.cs.
Example
var builder = WebApplication.CreateBuilder(args);
// Registering services
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddScoped<IReportService, ReportService>();
var app = builder.Build();
app.MapGet("/", (IReportService reportService) =>
{
reportService.GenerateReport();
return "Report Generated!";
});
app.Run();
3. Types of Dependency Injection
Constructor Injection (most common)
public class ReportService
{
private readonly IEmailSender _emailSender;
public ReportService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
}
Method Injection
public class NotificationService
{
public void Notify(IEmailSender sender)
{
sender.Send("Hello!");
}
}
Property Injection (less preferred)
public class OrderService
{
public IEmailSender EmailSender { get; set; }
}
4. Service Lifetimes Explained
When registering services, choose an appropriate lifetime.
Lifetime |
Description |
Example |
Transient |
New instance every time |
AddTransient |
Scoped |
One per request (web/API) |
AddScoped |
Singleton |
One instance for the app |
AddSingleton |
builder.Services.AddTransient<IService, MyService>(); // lightweight
builder.Services.AddScoped<IRepository, DbRepository>(); // web request
builder.Services.AddSingleton<ILogger, MyLogger>(); // config or caching
5. Injecting Services into Controllers or Endpoints
Example: ASP.NET Core Web API
[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
[HttpGet]
public IActionResult GetOrders()
{
var orders = _orderService.GetAll();
return Ok(orders);
}
}
6. Unit Testing with Dependency Injection
Mock dependencies using interfaces for testing.
var mockSender = new Mock<IEmailSender>();
mockSender.Setup(s => s.Send(It.IsAny<string>()));
var reportService = new ReportService(mockSender.Object);
reportService.GenerateReport();
DI makes testing cleaner and avoids hard-coded dependencies.
7. Best Practices
- Always program to interfaces, not implementations.
- Keep service lifetimes in sync with their dependencies.
- Avoid the ServiceLocator anti-pattern.
- Don't overuse a singleton for services using HttpContext or DB connections.
- Group service registrations using extension methods.
public static class ServiceCollectionExtensions
{
public static void AddMyAppServices(this IServiceCollection services)
{
services.AddScoped<IReportService, ReportService>();
services.AddTransient<IEmailSender, EmailSender>();
}
}
8. Common DI Errors
Error |
Cause |
Fix |
Cannot resolve service |
Missing registration |
Add builder.Services.AddX |
Lifetime mismatch |
Using a singleton inside a scope |
Change lifetime or use factory |
Null reference |
Optional dependencies not injected |
Use required services only |
Summary
.NET’s built-in dependency injection is fast, clean, and customizable.
By using DI,
- Your code becomes decoupled
- Testing becomes easier
- Maintenance becomes manageable
Start with simple constructor injection, register lifetimes properly, and evolve your architecture as needed.