Dependency Injection
Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC) for managing dependencies between objects. Rather than objects creating their own dependencies, they receive them from an external source. This promotes loose coupling, testability, and maintainable code.
Understanding Dependency Injection
The Restaurant Analogy
Imagine you're running a restaurant. Without DI, your chef would need to:
- Grow vegetables in a garden
- Raise chickens for eggs
- Mill flour for bread
- Manufacture cooking equipment
This is tightly coupled and impractical. With DI, you provide the chef with pre-sourced ingredients and equipment. The chef doesn't care where the ingredients come from—they just use what's provided to create dishes.
In code terms:
- Without DI: Classes create their own dependencies (the chef grows vegetables)
- With DI: Classes receive dependencies from outside (ingredients are provided)
Inversion of Control
Traditional programming flow: "I need a database connection, so I'll create one."
public class PlatformsController
{
private readonly AppDbContext _context;
public PlatformsController()
{
_context = new AppDbContext(); // ❌ Tightly coupled
}
}
Inversion of Control: "I need a database connection, someone else will provide it."
public class PlatformsController
{
private readonly AppDbContext _context;
public PlatformsController(AppDbContext context) // ✅ Dependency injected
{
_context = context;
}
}
Control is inverted: The controller no longer controls how its dependencies are created—that responsibility is inverted to an external container.
The .NET DI Container
ASP.NET Core includes a built-in DI container (also called the service container) that manages object creation and lifetime. It's lightweight, capable, and sufficient for most applications.
The container:
- Resolves dependencies: Creates objects and their dependencies automatically
- Manages lifetimes: Controls when objects are created and disposed
- Handles dependency graphs: Resolves nested dependencies recursively
When your application starts, services are registered in the container. When a class requests a dependency, the container automatically provides an instance.
Request → Controller needs IRepository
→ Container creates/provides IRepository instance
→ Controller uses IRepository
Registering Services
Services are registered in Program.cs using extension methods on IServiceCollection. This tells the container what to provide when a dependency is requested.
Basic Registration
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddScoped<ICommandRepository, CommandRepository>();
builder.Services.AddSingleton<IConfigService, ConfigService>();
builder.Services.AddTransient<IEmailService, EmailService>();
Registration Patterns
Interface to Implementation:
builder.Services.AddScoped<IPlatformRepository, PlatformRepository>();
// When someone requests IPlatformRepository, provide PlatformRepository
Concrete Types:
builder.Services.AddScoped<AppDbContext>();
// Register the concrete type directly
Factory Functions:
builder.Services.AddScoped<IApiClient>(provider =>
{
var config = provider.GetRequiredService<IConfiguration>();
var apiKey = config["ApiKeys:External"];
return new ApiClient(apiKey);
});
Service Lifetimes
The container manages three distinct lifetimes that control when instances are created and disposed.
Transient
Created every time they're requested.
builder.Services.AddTransient<IEmailService, EmailService>();
Characteristics:
- New instance per request
- Disposed after use
- Lightweight services with no state
- No shared data between requests
Use for:
- Stateless services
- Lightweight operations
- Services that shouldn't be shared
Example:
public class EmailService : IEmailService
{
public Task SendEmail(string to, string subject, string body)
{
// Send email logic
return Task.CompletedTask;
}
}
Scoped
Created once per HTTP request (or scope).
builder.Services.AddScoped<ICommandRepository, CommandRepository>();
builder.Services.AddScoped<AppDbContext>();
Characteristics:
- One instance per HTTP request
- Shared across the request pipeline
- Disposed at end of request
- Most common lifetime for web applications
Use for:
- Database contexts (EF Core)
- Repositories
- Unit of Work patterns
- Request-specific services
Example:
public class CommandRepository : ICommandRepository
{
private readonly AppDbContext _context;
public CommandRepository(AppDbContext context)
{
_context = context; // Same context throughout the request
}
}
Singleton
Created once for the application lifetime.
builder.Services.AddSingleton<ICacheService, CacheService>();
Characteristics:
- Single instance for entire application
- Created on first request (or at startup)
- Never disposed until application shuts down
- Shared across all requests and users
Use for:
- Configuration services
- Caching services
- Thread-safe stateless services
- Application-wide services
Example:
public class CacheService : ICacheService
{
private readonly ConcurrentDictionary<string, object> _cache = new();
public void Set(string key, object value) => _cache[key] = value;
public object? Get(string key) => _cache.TryGetValue(key, out var value) ? value : null;
}
Lifetime Comparison
| Lifetime | Created | Shared | Disposed | Use Case |
|---|---|---|---|---|
| Transient | Every injection | No | After use | Lightweight, stateless services |
| Scoped | Per HTTP request | Within request | End of request | Database contexts, repositories |
| Singleton | Once | Across entire app | Application shutdown | Caches, configuration, stateless utilities |
Lifetime Best Practices
Safe:
- Singleton → Singleton ✅
- Singleton → Transient ✅
- Scoped → Scoped ✅
- Scoped → Transient ✅
- Transient → Transient ✅
Unsafe (Captive Dependencies):
- Singleton → Scoped ❌ (Scoped service becomes singleton)
- Singleton → Transient ⚠️ (Transient service becomes singleton)
- Scoped → Singleton ✅ (But singleton must be thread-safe)
Injecting Dependencies
Dependencies are injected through constructors. The DI container automatically provides the required instances.
Constructor Injection
public class PlatformsController : ControllerBase
{
private readonly IPlatformRepository _repository;
private readonly ILogger<PlatformsController> _logger;
private readonly IMapper _mapper;
// DI container injects all dependencies
public PlatformsController(
IPlatformRepository repository,
ILogger<PlatformsController> logger,
IMapper mapper)
{
_repository = repository;
_logger = logger;
_mapper = mapper;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<PlatformReadDto>>> GetPlatforms()
{
_logger.LogInformation("Retrieving all platforms");
var platforms = await _repository.GetAllAsync();
return Ok(_mapper.Map<IEnumerable<PlatformReadDto>>(platforms));
}
}
Multiple Dependencies
The container resolves entire dependency graphs:
// Service A depends on B and C
public class ServiceA
{
public ServiceA(IServiceB serviceB, IServiceC serviceC) { }
}
// Service B depends on D
public class ServiceB : IServiceB
{
public ServiceB(IServiceD serviceD) { }
}
// When ServiceA is requested:
// Container creates ServiceD → ServiceB → ServiceC → ServiceA
Resolving Services Manually
Rarely needed, but possible using IServiceProvider:
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var repository = scope.ServiceProvider.GetRequiredService<IPlatformRepository>();
// Use repository
}
Benefits of Dependency Injection
Testability: Mock dependencies easily for unit testing
// Test with mock repository
var mockRepo = new Mock<IPlatformRepository>();
var controller = new PlatformsController(mockRepo.Object, ...);
- Loose Coupling: Classes depend on abstractions, not concrete implementations
- Maintainability: Change implementations without modifying consumers
- Flexibility: Swap implementations for different environments (e.g., mock services in development)
- Lifecycle Management: Container handles creation and disposal automatically
Common Patterns
Repository Pattern with DI
// Register
builder.Services.AddScoped<IPlatformRepository, PlatformRepository>();
builder.Services.AddScoped<ICommandRepository, CommandRepository>();
// Inject
public class PlatformsController : ControllerBase
{
private readonly IPlatformRepository _repository;
public PlatformsController(IPlatformRepository repository)
{
_repository = repository;
}
}
Service Layer Pattern
// Register
builder.Services.AddScoped<IPlatformService, PlatformService>();
// Service layer
public class PlatformService : IPlatformService
{
private readonly IPlatformRepository _repository;
private readonly IMapper _mapper;
public PlatformService(IPlatformRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
}
// Controller
public class PlatformsController : ControllerBase
{
private readonly IPlatformService _service;
public PlatformsController(IPlatformService service)
{
_service = service;
}
}
Dependency Injection is fundamental to modern .NET development. It's the backbone of ASP.NET Core's architecture and enables clean, testable, maintainable applications. Once you understand DI, you'll wonder how you ever coded without it.
Further Reading: