Skip to main content

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 IStudentService interface, provide StudentService implementation (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