Isaac.

Dependency Injection in ASP.NET Core

Learn about the principles of Dependency Injection and how to implement it in ASP.NET Core applications.

By EMEPublished: March 8, 2024
aspnetdependency injectionweb development

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!