C# Programming Tutorial 0/45 lessons ~6 min read Lesson 39

    Dependency Injection Basics

    Dependency Injection in ASP.NET Core registers services in Program.cs and resolves them via constructor injection.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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.
    text
    flowchart TB
    Startup[Program.cs] --> Services[AddServices]
    Services --> Container[IServiceProvider]
    Container --> Controller[Injected Controller]
    Controller --> Repo[IRepository impl]

    Step-by-step explanation

    1. WebApplicationBuilder.Services collects registrations.
    2. Build creates ServiceProvider.
    3. Controller constructor requests dependencies.
    4. Scoped services share within HTTP request.
    5. Singleton must not depend on Scoped — captive dependency.
    6. ValidateScopes in development catches lifetime bugs.

    Practical code example

    ASP.NET Core Program.cs service registration and scoped service consumption:

    csharp
    // Program.cs
    var 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 injection
    public 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 svc in 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?
    Transient new each; Scoped per scope/request; Singleton app-wide.
    Q2BeginnerWhy inject interfaces?
    Testability, swapping implementations, decoupling.
    Q3IntermediateCaptive dependency?
    Singleton holds Scoped instance past request — stale/disposed context.
    Q4IntermediateResolve scoped service from singleton?
    IServiceScopeFactory.CreateScope() per operation.
    Q5AdvancedDesign DI for multi-database tenant SaaS.
    Keyed ITenantDbContext by tenant id middleware; or factory; Scoped context selected per request from tenant resolver.

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.