Constructor Injection & Interfaces
Apply constructor injection with interface-based design to decouple components and enable unit testing.
Constructor Injection & Interfaces is a free C# Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Constructor Injection Basics
Constructor injection is the most common DI pattern. A class declares its dependencies as constructor parameters, and the container provides them at instantiation time.
This makes dependencies explicit, required, and visible.
Defining the Interface
Start with an interface that defines the contract. This allows different implementations (real, mock, in-memory) to be swapped without changing dependent code.
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task AddAsync(Product product);
}Implementing the Interface
The concrete implementation handles the real work — database calls, HTTP requests, etc. The consumer never needs to know which implementation it gets.
public class SqlProductRepository : IProductRepository
{
private readonly AppDbContext _db;
public SqlProductRepository(AppDbContext db) => _db = db;
public async Task<Product?> GetByIdAsync(int id)
=> await _db.Products.FindAsync(id);
public async Task<IEnumerable<Product>> GetAllAsync()
=> await _db.Products.ToListAsync();
public async Task AddAsync(Product product)
{
_db.Products.Add(product);
await _db.SaveChangesAsync();
}
}Consuming via Constructor
The service layer takes IProductRepository as a constructor parameter. It never knows about SqlProductRepository — just the interface contract.
public class ProductService
{
private readonly IProductRepository _repo;
private readonly ILogger<ProductService> _logger;
public ProductService(
IProductRepository repo,
ILogger<ProductService> logger)
{
_repo = repo;
_logger = logger;
}
public async Task<Product?> GetProductAsync(int id)
{
_logger.LogInformation("Fetching product {Id}", id);
return await _repo.GetByIdAsync(id);
}
}Registering the Chain
Register every service in the chain. The container resolves the full dependency graph automatically — ProductService → IProductRepository → AppDbContext.
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlite("Data Source=app.db"));
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();
builder.Services.AddScoped<ProductService>();Testing with a Mock
Because dependencies are injected via interfaces, unit tests can pass a mock or fake without touching the database. This is one of the biggest benefits of DI.
// Using NSubstitute or Moq in a unit test
var mockRepo = Substitute.For<IProductRepository>();
mockRepo.GetByIdAsync(1).Returns(new Product { Id = 1, Name = "Widget" });
var logger = Substitute.For<ILogger<ProductService>>();
var svc = new ProductService(mockRepo, logger);
var result = await svc.GetProductAsync(1);
Assert.Equal("Widget", result?.Name);Multiple Constructor Parameters
A class can have many constructor parameters. The container resolves all of them as long as each is registered. Keep constructors focused — too many parameters is a smell that the class has too many responsibilities.
public class OrderService
{
public OrderService(
IOrderRepository orders,
IProductRepository products,
IEmailSender email,
ILogger<OrderService> logger)
{
// all injected by DI container
}
}Avoiding Service Locator Anti-Pattern
Do NOT inject IServiceProvider and call GetService inside business methods. This hides dependencies and makes testing hard. Prefer constructor injection for explicit, testable code.
// BAD
public void Process(IServiceProvider sp)
{
var repo = sp.GetService<IProductRepository>(); // hidden dep!
}
// GOOD
public class Processor
{
private readonly IProductRepository _repo;
public Processor(IProductRepository repo) => _repo = repo;
}Primary Constructor Syntax (C# 12)
C# 12 introduces primary constructors for all class types, letting you declare parameters directly on the class declaration for concise DI code.
// C# 12 primary constructor
public class ProductService(
IProductRepository repo,
ILogger<ProductService> logger)
{
public async Task<Product?> GetAsync(int id)
{
logger.LogInformation("Getting {Id}", id);
return await repo.GetByIdAsync(id);
}
}Swapping Implementations
A key advantage of interface-based DI: you can swap an implementation in one line without changing any consuming code. Useful for switching databases, mailing providers, or feature flags.
// Switch from SQL to in-memory for integration tests
if (environment.IsEnvironment("Test"))
builder.Services.AddScoped<IProductRepository, InMemoryProductRepository>();
else
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();Real-World: Controller with DI
ASP.NET Core controllers use constructor injection natively. The framework resolves all parameters and passes them in.
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly ProductService _svc;
public ProductsController(ProductService svc) => _svc = svc;
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var product = await _svc.GetProductAsync(id);
return product is null ? NotFound() : Ok(product);
}
}Quick Check
What is the primary benefit of programming to interfaces when using DI?
Recap: Constructor Injection & Interfaces
Key takeaways:
- Declare dependencies as constructor parameters — the container provides them
- Define interfaces for service contracts; inject the interface, not the concrete type
- Interfaces enable easy mocking in unit tests and swapping implementations
- C# 12 primary constructors reduce boilerplate for DI-heavy classes
- Avoid the service locator anti-pattern — keep dependencies explicit
Frequently asked questions
Is the “Constructor Injection & Interfaces” lesson free?
Yes — the full text of “Constructor Injection & Interfaces” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “Constructor Injection & Interfaces”?
Apply constructor injection with interface-based design to decouple components and enable unit testing. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Constructor Injection & Interfaces” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- DI Container Fundamentals
- Service Lifetimes: Transient, Scoped, Singleton
- Constructor Injection & Interfaces
- Factory & Options Patterns