Dependency Injection (DI) is the practice of supplying an object’s dependencies from outside rather than creating them inside. Instead of a PostService calling new PostRepository(), the DI container creates PostRepository and passes it to PostService‘s constructor automatically. This inversion of control makes classes testable (swap the real repository for a mock), replaceable (swap SMTP email for SendGrid without touching service code), and clearly communicates what a class needs to function. ASP.NET Core’s built-in DI container is the backbone of the entire framework.
Registering Services
// ── The three registration lifetime methods ───────────────────────────────
// Scoped — one instance per HTTP request (most common for repositories, services)
builder.Services.AddScoped<IPostRepository, EfPostRepository>();
builder.Services.AddScoped<IPostService, PostService>();
// Singleton — one instance for the entire application lifetime
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
// Transient — new instance every time it is requested
builder.Services.AddTransient<ISlugGenerator, SlugGenerator>();
builder.Services.AddTransient<IPasswordHasher, BCryptPasswordHasher>();
// Register concrete type without interface
builder.Services.AddScoped<PostService>();
// EF Core DbContext — scoped by default
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseSqlServer(connectionString));
PostService, it inspects the constructor, sees that IPostRepository is needed, looks it up in the registrations, creates an EfPostRepository (which itself needs AppDbContext), and injects everything in the right order. This cascade resolution handles arbitrarily deep dependency graphs. The only requirement is that every dependency is registered — if a dependency is missing, the container throws InvalidOperationException: No service for type 'X' has been registered.IServiceCollection, one per layer or feature. builder.Services.AddApplicationServices() registers all application layer services; builder.Services.AddInfrastructure(config) registers all EF Core and external service integrations. This keeps Program.cs clean and makes it easy to see what each layer provides. It also makes integration tests easy — you can selectively register layers and swap individual registrations for test doubles.new to create services that have dependencies — this bypasses DI and makes testing impossible. The entire point of registering services in the DI container is so the container manages their creation. If you find yourself writing var repo = new EfPostRepository(new AppDbContext(...)) inside a service method, you are manually re-implementing what the DI container does automatically — remove it and inject the repository through the constructor instead.Constructor Injection Pattern
// ── Service with multiple injected dependencies ───────────────────────────
public class PostService(
IPostRepository repo,
IEmailSender email,
ILogger<PostService> logger,
ICacheService cache) // primary constructor (C# 12)
{
public async Task<Post> GetByIdAsync(int id, CancellationToken ct = default)
{
if (cache.TryGet($"post:{id}", out Post? cached))
return cached!;
logger.LogDebug("Cache miss for post {Id}.", id);
var post = await repo.GetByIdAsync(id, ct)
?? throw new NotFoundException(nameof(Post), id);
cache.Set($"post:{id}", post);
return post;
}
public async Task<Post> PublishAsync(int id, string authorEmail, CancellationToken ct = default)
{
var post = await GetByIdAsync(id, ct);
post.Publish();
await repo.UpdateAsync(post, ct);
await email.SendAsync(authorEmail, "Your post is live!", $"'{post.Title}' is now published.");
return post;
}
}
// ── Controller — DI injects service automatically ─────────────────────────
[ApiController]
[Route("api/posts")]
public class PostsController(IPostService service) : ControllerBase
{
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id, CancellationToken ct)
=> Ok(await service.GetByIdAsync(id, ct));
[HttpPost("{id:int}/publish")]
public async Task<IActionResult> Publish(int id, CancellationToken ct)
=> Ok(await service.PublishAsync(id, User.GetEmail(), ct));
}
Common Mistakes
Mistake 1 — Using new inside a service (bypasses DI)
❌ Wrong — hardcodes the concrete type, test-impossible:
private readonly IPostRepository _repo = new EfPostRepository(new AppDbContext(...));
✅ Correct — declare as a constructor parameter; let the container inject it.
Mistake 2 — Registering the same interface twice (last registration wins)
❌ Confusing — only the last registration for a given type is resolved by default:
services.AddScoped<IEmailSender, SmtpSender>();
services.AddScoped<IEmailSender, SendGridSender>(); // SmtpSender never resolved!
✅ Correct — register each interface once; use conditional registration for environment-specific implementations.