Dependency Injection in ASP.NET Core
Learn about the principles of Dependency Injection and how to implement it in ASP.NET Core applications.
Dependency Injection in ASP.NET Core
Dependency Injection (DI) is a fundamental design pattern that decouples classes and promotes testability and flexibility in your applications. ASP.NET Core has built-in DI container support.
What is Dependency Injection?
Dependency Injection is a technique where objects receive their dependencies from external sources rather than creating them internally. Instead of a class creating the objects it needs, these dependencies are "injected" into the class.
Without Dependency Injection
public class UserService
{
private readonly DatabaseContext _db;
public UserService()
{
// Class creates its own dependency
_db = new DatabaseContext();
}
public User GetUser(int id) => _db.Users.Find(id);
}
Problems:
- Tightly coupled to
DatabaseContext - Hard to test (can't use mock database)
- Difficult to swap implementations
- Difficult to configure database connection
With Dependency Injection
public class UserService
{
private readonly DatabaseContext _db;
// Dependency is injected via constructor
public UserService(DatabaseContext db)
{
_db = db;
}
public User GetUser(int id) => _db.Users.Find(id);
}
Benefits:
- Loosely coupled
- Easy to test with mock objects
- Easy to swap implementations
- Configuration centralized
Dependency Injection in ASP.NET Core
1. Service Registration
Register services in Program.cs:
var builder = WebApplicationBuilder.CreateBuilder(args);
// Transient: New instance every time
builder.Services.AddTransient<IEmailService, EmailService>();
// Scoped: One instance per request
builder.Services.AddScoped<DatabaseContext>();
// Singleton: Single instance for application lifetime
builder.Services.AddSingleton<ICache, MemoryCache>();
var app = builder.Build();
2. Service Lifetimes
Transient
builder.Services.AddTransient<IRepository, Repository>();
- New instance created every time it's requested
- Use for stateless services
- Good for lightweight services
Scoped
builder.Services.AddScoped<DatabaseContext>();
- One instance per HTTP request
- Use for database contexts (Entity Framework)
- Default for web applications
Singleton
builder.Services.AddSingleton<IAppSettings, AppSettings>();
- Single instance for entire application
- Shared across all requests
- Use for stateless, thread-safe services
- Good for caching
3. Injecting Dependencies
Constructor Injection
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
// Dependency injected via constructor
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> GetUser(int id)
{
var user = await _userService.GetUserAsync(id);
if (user == null)
return NotFound();
return Ok(user);
}
}
Method Injection (Less Common)
public void ConfigureServices(IServiceProvider services)
{
var logger = services.GetRequiredService<ILogger<Startup>>();
}
Creating Serviceable Interfaces
Best practice is to depend on abstractions (interfaces):
// Define interface
public interface IUserService
{
Task<User> GetUserAsync(int id);
Task<User> CreateUserAsync(CreateUserDto dto);
Task UpdateUserAsync(int id, UpdateUserDto dto);
Task DeleteUserAsync(int id);
}
// Implement interface
public class UserService : IUserService
{
private readonly DatabaseContext _db;
public UserService(DatabaseContext db)
{
_db = db;
}
public async Task<User> GetUserAsync(int id)
{
return await _db.Users.FindAsync(id);
}
public async Task<User> CreateUserAsync(CreateUserDto dto)
{
var user = new User { Name = dto.Name, Email = dto.Email };
_db.Users.Add(user);
await _db.SaveChangesAsync();
return user;
}
public async Task UpdateUserAsync(int id, UpdateUserDto dto)
{
var user = await _db.Users.FindAsync(id);
if (user != null)
{
user.Name = dto.Name;
user.Email = dto.Email;
await _db.SaveChangesAsync();
}
}
public async Task DeleteUserAsync(int id)
{
var user = await _db.Users.FindAsync(id);
if (user != null)
{
_db.Users.Remove(user);
await _db.SaveChangesAsync();
}
}
}
// Register in Program.cs
builder.Services.AddScoped<IUserService, UserService>();
Advanced Scenarios
Factory Registration
builder.Services.AddScoped<IUserService>(provider =>
{
var dbContext = provider.GetRequiredService<DatabaseContext>();
var logger = provider.GetRequiredService<ILogger<UserService>>();
return new UserService(dbContext, logger);
});
Conditional Registration
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IEmailService, ConsoleEmailService>();
}
else
{
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
}
Multiple Implementations
// Register multiple implementations
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
builder.Services.AddScoped<INotificationService, SmsNotificationService>();
// Inject as collection
public class NotificationManager
{
private readonly IEnumerable<INotificationService> _services;
public NotificationManager(IEnumerable<INotificationService> services)
{
_services = services;
}
public async Task NotifyAsync(string message)
{
foreach (var service in _services)
{
await service.SendAsync(message);
}
}
}
Named/Keyed Services (ASP.NET Core 8+)
builder.Services.AddKeyedScoped<IRepository>("primary",
(provider, key) => new PrimaryRepository());
builder.Services.AddKeyedScoped<IRepository>("cache",
(provider, key) => new CachedRepository());
// Usage
public class UserService
{
public UserService(
[FromKeyedServices("primary")] IRepository primaryRepo,
[FromKeyedServices("cache")] IRepository cacheRepo)
{
// Use both repositories
}
}
Testing with Dependency Injection
DI makes unit testing much easier:
[TestClass]
public class UserServiceTests
{
private Mock<DatabaseContext> _mockDb;
private UserService _service;
[TestInitialize]
public void Setup()
{
// Create mock instead of real database
_mockDb = new Mock<DatabaseContext>();
_service = new UserService(_mockDb.Object);
}
[TestMethod]
public async Task GetUserAsync_WithValidId_ReturnsUser()
{
// Arrange
var userId = 1;
var expectedUser = new User { Id = userId, Name = "Test User" };
_mockDb.Setup(d => d.Users.FindAsync(userId))
.ReturnsAsync(expectedUser);
// Act
var result = await _service.GetUserAsync(userId);
// Assert
Assert.AreEqual(expectedUser.Name, result.Name);
}
}
Best Practices
1. Depend on Abstractions, Not Concrete Types
✅ Good:
public UserController(IUserService service) { }
❌ Bad:
public UserController(UserService service) { }
2. Prefer Constructor Injection
✅ Good:
public class MyService
{
private readonly ILogger<MyService> _logger;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
}
3. Use Appropriate Lifetimes
- Transient: Stateless services
- Scoped: Database contexts, repositories
- Singleton: Stateless, thread-safe services
4. Register at Startup
Don't create dependencies manually; register them in Program.cs.
5. Avoid Service Locator Pattern
❌ Bad:
var service = ServiceProvider.GetService<IUserService>();
✅ Good:
public MyClass(IUserService service)
{
_service = service;
}
Common Issues
Service Not Registered
InvalidOperationException: Unable to resolve service for type 'IUserService'
Solution: Register the service in Program.cs.
Circular Dependencies
If Service A depends on Service B and vice versa, refactor to break the cycle.
Missing Dependencies
Make sure all injected services are registered with appropriate lifetimes.
Conclusion
Dependency Injection is essential for building maintainable, testable ASP.NET Core applications. ASP.NET Core's built-in DI container provides everything you need without external packages.
Key takeaways:
- Use interfaces for loose coupling
- Register services with appropriate lifetimes
- Inject dependencies via constructors
- Test with mock implementations
- Follow established best practices
Start using DI from day one of your ASP.NET Core projects for better code quality!