Dependency Injection
Level: Intermediate
ℹ️ What You'll Learn
- Tight coupling problem:
new StudentService()inside controller hard-codes dependency, hard to test - Loose coupling solution: Inject dependency via constructor, flexible to swap implementations
- Dependency Injection principle: Framework provides dependencies instead of creating them manually
- Service registration:
builder.Services.AddScoped<IStudentService, StudentService>()in Program.cs - Transient lifetime: New instance every time (stateless utilities,
AddTransient<>) - Scoped lifetime: New instance per HTTP request (EF Core DbContext, services using database)
- Singleton lifetime: Single instance for application lifetime (configuration, logging, caches)
- Constructor injection pattern:
public StudentsController(IStudentService service)receives service automatically - Interface vs implementation: Register
IStudentServiceinterface, provideStudentServiceimplementation (flexible!) - Service resolution: Framework finds all constructor parameters, injects matching registered services
- Testability benefit: Mock service in tests (
var mockService = new Mock<IStudentService>()) - School Management services: StudentService, TeacherService, ExamService, FeeService, AuthService (all injected)
- Service locator antipattern: Don't use
IServiceProvider.GetService()(use constructor injection instead) - Circular dependency: If Service A depends on Service B and vice versa, you have a design problem
DI Problem
Tight coupling (bad):
public class StudentController
{
private StudentService _service = new StudentService(); // Hard dependency
}
Loose coupling (good):
public class StudentController
{
private readonly IStudentService _service;
public StudentController(IStudentService service)
{
_service = service; // Injected dependency
}
}
Register Services (Program.cs)
// Add services
builder.Services.AddScoped<IStudentService, StudentService>();
builder.Services.AddScoped<IExamService, ExamService>();
builder.Services.AddScoped<IFeeService, FeeService>();
// Database context
builder.Services.AddScoped<SmsDbContext>();
// Repositories (optional)
builder.Services.AddScoped<IStudentRepository, StudentRepository>();
Lifetimes
Singleton = same instance always
builder.Services.AddSingleton<IConfigService, ConfigService>();
Use for: stateless services, caching
Scoped = new instance per request
builder.Services.AddScoped<IStudentService, StudentService>();
Use for: database context, repositories
Transient = new instance every time
builder.Services.AddTransient<ILogger, ConsoleLogger>();
Use for: lightweight objects
Injection in Controller
public class StudentsController : ControllerBase
{
private readonly IStudentService _studentService;
private readonly IExamService _examService;
private readonly ILogger<StudentsController> _logger;
// Constructor injection
public StudentsController(
IStudentService studentService,
IExamService examService,
ILogger<StudentsController> logger)
{
_studentService = studentService;
_examService = examService;
_logger = logger;
}
[HttpGet("{id}")]
public async Task<ActionResult<Student>> GetStudent(int id)
{
_logger.LogInformation("Getting student {Id}", id);
var student = await _studentService.GetStudentAsync(id);
if (student == null)
return NotFound();
return Ok(student);
}
}
Interface-Based Services
// Define interface
public interface IStudentService
{
Task<Student> GetStudentAsync(int id);
Task CreateStudentAsync(Student student);
}
// Implement interface
public class StudentService : IStudentService
{
private readonly SmsDbContext _context;
public StudentService(SmsDbContext context)
{
_context = context;
}
public async Task<Student> GetStudentAsync(int id)
{
return await _context.Students.FindAsync(id);
}
public async Task CreateStudentAsync(Student student)
{
_context.Students.Add(student);
await _context.SaveChangesAsync();
}
}
Benefits
- Testability — Mock dependencies
- Loose coupling — Easy to swap implementations
- Flexibility — Different implementations for different scenarios
- Maintainability — Clear dependencies
Testing with DI
[TestFixture]
public class StudentServiceTests
{
[Test]
public async Task GetStudent_ReturnsStudent()
{
// Mock repository
var mockRepository = new Mock<IStudentRepository>();
mockRepository
.Setup(r => r.GetStudentAsync(It.IsAny<int>()))
.ReturnsAsync(new Student { Id = 101, Name = "Ravi" });
// Inject mock
var service = new StudentService(mockRepository.Object);
// Test
var result = await service.GetStudentAsync(101);
Assert.AreEqual("Ravi", result.Name);
}
}
Key Takeaways
- DI = inject dependencies
- Loose coupling = flexible code
- Scoped = per request
- Singleton = global instance
- Transient = new each time
💡 DI Tip
Depend on abstractions (interfaces), not implementations.
🤖Use AI to Learn Faster
Use ChatGPT, Claude, or Copilot to go deeper on Dependency Injection. Try these prompts:
"What's the difference between Scoped and Singleton?""Why use interfaces for services?""How do I mock services for testing?""Quiz me on DI"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.
nexcoding.in
Have questions on your tech stack, ongoing projects, or need one-to-one training?