Understanding Dependency Injection in .NET (A Practical Guide)
Introduction
Dependency Injection (DI) is a design pattern that helps you build loosely coupled, testable, and maintainable applications. In .NET and ASP.NET Core, DI is built-in and used heavily in modern app architecture.
Why DI matters
- Improves testability: you can mock dependencies easily.
- Promotes single responsibility and separation of concerns.
- Makes it easier to replace implementations (e.g., swap a file logger with a cloud logger).
Core concept
Instead of a class creating its own dependencies, dependencies are provided to it (injected) from the outside.
Types of injection
- Constructor injection — most common and recommended.
- Method injection — pass dependencies as method parameters.
- Property injection — set dependencies via properties (less preferred).
Example in ASP.NET Core (Constructor Injection)
Register services in Program.cs or Startup.cs:
// Program.cs (minimal hosting model)
var builder = WebApplication.CreateBuilder(args);
// Register a service and its implementation
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
var app = builder.Build();
Inject and use in a controller or other class:
public interface INotificationService
{
Task SendAsync(string to, string subject, string body);
}
public class EmailNotificationService : INotificationService
{
public Task SendAsync(string to, string subject, string body)
{
// send email (pseudo)
Console.WriteLine($"Sending email to {to}");
return Task.CompletedTask;
}
}
public class UsersController : ControllerBase
{
private readonly INotificationService _notificationService;
// Constructor injection
public UsersController(INotificationService notificationService)
{
_notificationService = notificationService;
}
[HttpPost("notify")]
public async Task<IActionResult> NotifyUser(string email)
{
await _notificationService.SendAsync(email, "Hello", "Welcome!");
return Ok("Notified");
}
}
Service lifetimes
When registering services, choose the appropriate lifetime:
Transient— new instance every time requestedScoped— one instance per request (good for web apps)Singleton— one instance for the application lifetime
Testing with DI
Because your classes depend on interfaces, you can supply mock implementations in unit tests:
// Example using Moq
var mockNotification = new Mock<INotificationService>();
mockNotification.Setup(m => m.SendAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.CompletedTask);
var controller = new UsersController(mockNotification.Object);
await controller.NotifyUser("test@example.com");
mockNotification.Verify(m => m.SendAsync("test@example.com", It.IsAny<string>(), It.IsAny<string>()), Times.Once);
Best practices
- Depend on abstractions (interfaces), not concrete types.
- Prefer constructor injection for required dependencies.
- Avoid service locator pattern (it hides dependencies).
- Keep service lifetimes appropriate to work (e.g., DbContext is Scoped).
Conclusion
DI is an essential pattern in .NET that leads to cleaner, testable code. ASP.NET Core makes it easy with built-in DI container — use interfaces, choose correct lifetimes, and write unit tests for better reliability.
If you’d like, I can also add a downloadable cheat-sheet or sample repo link for readers — want that?

