Dependency Injection (DI) is the mechanism ASP.NET Core uses to supply objects with the dependencies they need. Instead of a service creating its own dependencies with new, they are passed in through the constructor — and the DI container creates and wires everything automatically. Interfaces are the glue that makes this work: the container maps an interface to a concrete implementation, and consuming code only ever sees the interface. When you swap implementations (real database vs in-memory test db, real email vs fake email), the consuming code never changes.
Interface + Implementation + Registration
// ── Step 1: Define the interface contract ────────────────────────────────
public interface IPostRepository
{
Task<Post?> GetByIdAsync(int id);
Task<List<Post>> GetAllAsync(int page = 1, int pageSize = 10);
Task<Post> CreateAsync(Post post);
Task<Post> UpdateAsync(Post post);
Task DeleteAsync(int id);
Task<bool> ExistsAsync(int id);
}
// ── Step 2: Write the concrete implementation ─────────────────────────────
public class PostRepository : IPostRepository
{
private readonly AppDbContext _db;
public PostRepository(AppDbContext db) => _db = db;
public async Task<Post?> GetByIdAsync(int id)
=> await _db.Posts.FindAsync(id);
public async Task<List<Post>> GetAllAsync(int page = 1, int pageSize = 10)
=> await _db.Posts
.OrderByDescending(p => p.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
public async Task<Post> CreateAsync(Post post)
{
_db.Posts.Add(post);
await _db.SaveChangesAsync();
return post;
}
public async Task<Post> UpdateAsync(Post post)
{
_db.Posts.Update(post);
await _db.SaveChangesAsync();
return post;
}
public async Task DeleteAsync(int id)
{
var post = await _db.Posts.FindAsync(id);
if (post is not null)
{
_db.Posts.Remove(post);
await _db.SaveChangesAsync();
}
}
public async Task<bool> ExistsAsync(int id)
=> await _db.Posts.AnyAsync(p => p.Id == id);
}
// ── Step 3: Register in Program.cs ───────────────────────────────────────
builder.Services.AddScoped<IPostRepository, PostRepository>();
// IPostRepository (interface) → PostRepository (implementation)
// AddScoped = one instance per HTTP request
Program.cs using extension methods to keep the file readable. Create a static class like ServiceCollectionExtensions with a method AddApplicationServices(this IServiceCollection services) that contains all your service registrations. Then call builder.Services.AddApplicationServices() in Program.cs. This follows the same pattern used by ASP.NET Core itself — builder.Services.AddControllers(), builder.Services.AddDbContext() are all extension methods doing exactly this.new to create service dependencies inside a class — this bypasses DI, hardcodes the concrete type, and makes the class impossible to unit test without the real dependency. private readonly IPostRepository _repo = new PostRepository(db); is the anti-pattern. The correct pattern always receives dependencies through the constructor: public PostService(IPostRepository repo) { _repo = repo; }. The DI container injects the right implementation automatically.Constructor Injection Pattern
// ── Service depends only on the interface, not the implementation ─────────
public class PostService
{
private readonly IPostRepository _repo;
private readonly IEmailSender _emailSender;
private readonly ILogger<PostService> _logger;
// DI container calls this constructor automatically
public PostService(
IPostRepository repo,
IEmailSender emailSender,
ILogger<PostService> logger)
{
_repo = repo;
_emailSender = emailSender;
_logger = logger;
}
public async Task<Post> PublishAsync(int postId, string authorEmail)
{
var post = await _repo.GetByIdAsync(postId)
?? throw new KeyNotFoundException($"Post {postId} not found.");
post.IsPublished = true;
post.PublishedAt = DateTime.UtcNow;
await _repo.UpdateAsync(post);
_logger.LogInformation("Post {PostId} published by {Author}", postId, authorEmail);
await _emailSender.SendAsync(
authorEmail,
"Your post is live!",
$"'{post.Title}' is now published.");
return post;
}
}
// Register PostService itself
builder.Services.AddScoped<IPostService, PostService>();
// Testing is trivial — inject mocks
// var service = new PostService(mockRepo, mockEmail, mockLogger);
Common Mistakes
Mistake 1 — Using new inside a service (bypasses DI)
❌ Wrong:
public class PostService
{
private readonly PostRepository _repo = new PostRepository(???); // impossible to test!
✅ Correct — always receive dependencies through the constructor.
Mistake 2 — Injecting a Scoped service into a Singleton
❌ Wrong — the Scoped service lives as long as the Singleton (entire app lifetime), leaking state between requests.
✅ Correct — inject IServiceScopeFactory into the Singleton and create a scope explicitly when you need a Scoped service.