Dependency Injection Basics
Dependency Injection in ASP.NET Core registers services in Program.cs and resolves them via constructor injection.
Introduction
Dependency Injection in ASP.NET Core registers services in Program.cs and resolves them via constructor injection. Lifetimes — Singleton, Scoped, Transient — control instance sharing and are a favorite interview topic.
IServiceCollection extension methods organize registration. Avoid service locator anti-pattern. Test projects replace services with mocks using WebApplicationFactory.
The story
A order lookup API endpoint receives HTTP requests, resolves an OrderService from the DI container, and calls the repository — all without new EfOrderRepository() in the handler. Scoped lifetimes mean each HTTP request gets its own database context, preventing stale data and disposed-context bugs.
Understanding the topic
Key concepts
- Register: services.AddScoped
(); - Resolve via constructor parameters automatically.
- Singleton one instance; Scoped per request; Transient every resolve.
- IServiceProvider root; create scope for background work.
- Options pattern IOptions
for configuration. - Keyed services (.NET 8) for multi-implementation.
flowchart TBStartup[Program.cs] --> Services[AddServices]Services --> Container[IServiceProvider]Container --> Controller[Injected Controller]Controller --> Repo[IRepository impl]
Step-by-step explanation
- WebApplicationBuilder.Services collects registrations.
- Build creates ServiceProvider.
- Controller constructor requests dependencies.
- Scoped services share within HTTP request.
- Singleton must not depend on Scoped — captive dependency.
- ValidateScopes in development catches lifetime bugs.
Practical code example
ASP.NET Core Program.cs service registration and scoped service consumption:
// Program.csvar builder = WebApplication.CreateBuilder(args);builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();builder.Services.AddScoped<OrderService>();builder.Services.AddDbContext<AppDbContext>(o =>o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));var app = builder.Build();app.MapGet("/orders/{id:guid}", async (Guid id, OrderService svc, CancellationToken ct) =>{var order = await svc.GetAsync(id, ct);return order is null ? Results.NotFound() : Results.Ok(order);});app.Run();// OrderService.cs — constructor injectionpublic sealed class OrderService(IOrderRepository repo){public Task<Order?> GetAsync(Guid id, CancellationToken ct) => repo.FindAsync(id, ct);}
Line-by-line code explanation
WebApplication.CreateBuilder(args)bootstraps the ASP.NET Core host and service collection.builder.Services.AddScoped<IOrderRepository, EfOrderRepository>()registers interface-to-implementation mapping per request.AddScoped<OrderService>()registers the application service at scoped lifetime.AddDbContext<AppDbContext>(... UseNpgsql(...))configures EF Core with PostgreSQL connection string.var app = builder.Build()constructs the middleware pipeline and service provider.app.MapGet("/orders/{id:guid}", ...)defines a minimal API endpoint with route constraint.OrderService svcin the handler signature is resolved automatically from DI.await svc.GetAsync(id, ct)delegates to the injected service layer.order is null ? Results.NotFound() : Results.Ok(order)returns appropriate HTTP status codes.OrderService(IOrderRepository repo)uses constructor injection — dependencies are explicit and testable.
Key takeaway: Minimal API resolves OrderService from DI per request. Scoped DbContext aligns with request scope. Never inject Scoped into Singleton.
Real-world use
Where you'll use this in production
- ASP.NET Core web API service wiring.
- Worker services IHostedService with scoped factory.
- Testing with replacement ITwitterClient fake.
- Multi-tenant keyed database contexts.
Best practices
- Constructor inject interfaces not concretions.
- Match lifetime to usage — DbContext Scoped.
- Use IOptionsMonitor for reloadable config.
- Register validators, repositories, handlers explicitly.
- Enable ValidateOnBuild for early missing registration detection.
Common mistakes
- Captive dependency Singleton→Scoped.
- Service locator IServiceProvider.GetService in domain.
- Registering DbContext as Singleton.
- Circular dependency without refactor or lazy.
Advanced interview questions
Q1BeginnerThree DI lifetimes?
Q2BeginnerWhy inject interfaces?
Q3IntermediateCaptive dependency?
Q4IntermediateResolve scoped service from singleton?
Q5AdvancedDesign DI for multi-database tenant SaaS.
Summary
ASP.NET Core DI registers and resolves services automatically. Choose correct lifetime — Scoped for DbContext. Constructor injection keeps dependencies explicit. Avoid service locator and captive dependencies. Next: building HTTP APIs.