• Courses
  • TutorialsFree
  • Learning Paths
  • Blogs
  • Authors
  • Dometrain Pro
  • Shopping Basket

    Your basket is empty

  • Business Portal
  • Getting Started with Dependency Injection in .NET

    8/12/2025

    Good DI makes code easier to change and easier to test. In .NET you get a container out of the box, so you don't need extra packages to start. The idea is simple: depend on abstractions, register implementations once, and let the runtime supply them where needed.

    This guide focuses on the built‑in container, Minimal APIs, and practical code you can paste into a project today.


    What DI is (in .NET terms)

    • You register services on IServiceCollection.
    • At runtime, ASP.NET Core creates an IServiceProvider to resolve them.
    • You inject dependencies via constructor (preferred) or as parameters in Minimal API handlers.

    No factories sprinkled through the code, no new chains inside your endpoints.


    Lifetimes cheat sheet

    Lifetime Created Typical use Notes
    Transient Every resolve Stateless mappers, formatters, small services Short‑lived; cheap to create
    Scoped Once per request Repositories, DbContext, units of work The default for web request‑bound work
    Singleton Once per app Caches, config readers, heavy single instances Avoid capturing scoped services (captive dependency)

    Rule of thumb: start Scoped for request‑bound services, Transient for tiny helpers, Singleton for pure stateless services that are safe to share (thread‑safe + no per‑request state).


    Minimal API setup

    Program.cs:

    var builder = WebApplication.CreateBuilder(args);
    var services = builder.Services;
    
    // registrations
    services.AddScoped<IOrderRepository, EfOrderRepository>();
    services.AddScoped<CreateOrderHandler>();
    services.AddSingleton<ISystemClock, SystemClock>(); // safe singleton
    services.AddDbContext<AppDbContext>(o => o.UseNpgsql(
        builder.Configuration.GetConnectionString("Default")));
    
    var app = builder.Build();
    
    // endpoints
    app.MapPost("/orders", async Task<IResult> (CreateOrderRequest req, CreateOrderHandler handler) =>
    {
        var id = await handler.Handle(req);
        return Results.Created($"/orders/{id}", new { id });
    });
    
    app.MapGet("/orders/{id:guid}", async Task<IResult> (Guid id, IOrderRepository repo) =>
    {
        var dto = await repo.GetDetailsAsync(id);
        return dto is null ? Results.NotFound() : Results.Ok(dto);
    });
    
    app.Run();
    
    • Handlers are plain classes; endpoints stay thin.
    • Dependencies appear as parameters and are resolved by the container.

    Constructor injection in application code

    public sealed class CreateOrderHandler(IOrderRepository repo, ISystemClock clock)
    {
        public async Task<Guid> Handle(CreateOrderRequest req)
        {
            var now = clock.UtcNow;
            var order = Order.Create(req.CustomerId, now, req.Lines);
            await repo.AddAsync(order);
            return order.Id;
        }
    }
    

    This class has no idea about DI. It only knows about its interfaces.


    Service registration examples

    services.AddTransient<ISlugGenerator, SlugGenerator>();
    services.AddScoped<IEmailSender, SmtpEmailSender>();
    services.AddSingleton<ISystemClock, SystemClock>(); // wraps DateTime.UtcNow
    
    // open generics
    services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
    
    // named/typed HttpClient (uses built-in factory)
    services.AddHttpClient("github", c =>
    {
        c.BaseAddress = new Uri("https://api.github.com");
        c.DefaultRequestHeaders.UserAgent.ParseAdd("HelloApi/1.0");
    });
    

    Avoid the service locator trap

    Yes, you can resolve services manually:

    var sp = app.Services;
    using var scope = sp.CreateScope();
    var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
    

    Where this is okay: at the composition root (startup, background wiring, one‑off tasks).
    Where to avoid it: inside business classes. It hides dependencies and complicates tests. Prefer constructor injection.


    Captive dependency (common lifetime bug)

    If a Singleton depends on a Scoped service, the scoped instance may be cached for the whole app lifetime — bad news for things like DbContext.

    services.AddSingleton<ReportCache>();     // BAD if ReportCache needs DbContext
    services.AddScoped<AppDbContext>();
    

    Fixes:

    • Make the top service Scoped as well, or
    • Inject a factory instead of the scoped service:
    public sealed class ReportCache(Func<AppDbContext> dbFactory)
    {
        public async Task<int> CountAsync() 
        {
            using var db = dbFactory();
            return await db.Orders.CountAsync();
        }
    }
    

    And register:

    services.AddScoped<ReportCache>();
    services.AddScoped<Func<AppDbContext>>(sp => () => sp.GetRequiredService<AppDbContext>());
    

    Options: configuration as a service

    builder.Services.Configure<MailOptions>(
        builder.Configuration.GetSection("Mail"));
    
    public sealed class MailOptions
    {
        public string Host { get; init; } = "";
        public int Port { get; init; }
    }
    
    public sealed class SmtpEmailSender(IOptions<MailOptions> options)
        : IEmailSender
    {
        private readonly MailOptions _opt = options.Value;
        // use _opt.Host / _opt.Port
    }
    

    If you expect changes at runtime, inject IOptionsSnapshot<T> (scoped) or IOptionsMonitor<T> (singleton with change notifications).


    Testing with DI (quick sketch)

    Create a handler with fakes; no container needed.

    public sealed class FakeClock(DateTime fixedUtc) : ISystemClock
    {
        public DateTime UtcNow { get; } = fixedUtc;
    }
    
    [Fact]
    public async Task Creates_order_with_now()
    {
        var repo = new InMemoryOrderRepository();
        var clock = new FakeClock(new DateTime(2025, 1, 1, 12, 0, 0, DateTimeKind.Utc));
        var sut = new CreateOrderHandler(repo, clock);
    
        var id = await sut.Handle(new CreateOrderRequest(/* ... */));
    
        var saved = await repo.GetDetailsAsync(id);
        saved!.CreatedAtUtc.ShouldBe(clock.UtcNow); // Shouldly
    }
    

    If you prefer to spin up the container in tests, use WebApplicationFactory or build a small ServiceCollection and call BuildServiceProvider().


    Minimal recipes you’ll reuse

    Decorator without extra libraries:

    services.AddScoped<IOrderRepository, EfOrderRepository>();
    services.AddScoped<IOrderRepository>(sp =>
    {
        var inner = sp.GetRequiredService<EfOrderRepository>();
        var log = sp.GetRequiredService<ILogger<OrderRepositoryLoggingDecorator>>();
        return new OrderRepositoryLoggingDecorator(inner, log);
    });
    
    public sealed class OrderRepositoryLoggingDecorator(IOrderRepository inner, ILogger logger)
        : IOrderRepository
    {
        public async Task<OrderDto?> GetDetailsAsync(Guid id)
        {
            logger.LogInformation("Fetching order {Id}", id);
            return await inner.GetDetailsAsync(id);
        }
        // pass-through other members
    }
    

    Background service that uses scoped services:

    public sealed class CleanupWorker(IServiceScopeFactory scopeFactory, ILogger<CleanupWorker> log)
        : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken ct)
        {
            while (!ct.IsCancellationRequested)
            {
                using var scope = scopeFactory.CreateScope();
                var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    
                await CleanupAsync(db, ct);
                await Task.Delay(TimeSpan.FromMinutes(10), ct);
            }
        }
    }
    

    Register it with services.AddHostedService<CleanupWorker>();


    Final notes

    • Prefer constructor injection. It keeps dependencies visible and testable.
    • Pick lifetimes with intent; watch out for captive dependencies.
    • Keep endpoints thin; move logic to classes that accept interfaces.
    • Use the container in the composition root; avoid pulling it into business code.

    About the Author

    Nick Chapsas

    Nick Chapsas

    Nick Chapsas is a .NET & C# content creator, educator and a Microsoft MVP for Developer Technologies with years of experience in Software Engineering and Engineering Management.

    He has worked for some of the biggest companies in the world, building systems that served millions of users and tens of thousands of requests per second.

    Nick creates free content on YouTube and is the host of the Keep Coding Podcast.

    View all courses by Nick Chapsas

    What's New

    What's new in C# 14
    blog

    What's new in C# 14

    This guide covers every new C# 14 feature, explains its benefits, and provides practical code examples to help you navigate how you can use them.

    Learn More
    Let's Build It: AI Chatbot with RAG in .NET Using Your Data
    course

    Let's Build It: AI Chatbot with RAG in .NET Using Your Data

    Build a Retrieval-Augmented Generation (RAG) chatbot that can answer questions using your data.

    Learn More
    Working with Large Language Models
    tutorial

    Working with Large Language Models

    Learn how to work with Large Language Models (LLMs). Understand the fundamentals of how GPT works, the transformer architecture, and master prompt engineering techniques to build AI agents.

    Learn More
    From Zero to Hero: SignalR in .NET
    course

    From Zero to Hero: SignalR in .NET

    Enable enterprise-grade real-time communication for your web apps with SignalR.

    Learn More
    Deep Dive: Solution Architecture
    course

    Deep Dive: Solution Architecture

    Master solution architecture and turn business needs into scalable, maintainable systems.

    Learn More
    Migrating: ASP.NET Web APIs to ASP.NET Core
    course

    Migrating: ASP.NET Web APIs to ASP.NET Core

    A step-by-step process to migrate ASP.NET Web APIs from .NET Framework to ASP.NET Core.

    Learn More
    Getting Started: Caching in .NET
    course

    Getting Started: Caching in .NET

    Let's make the hardest thing in programming easy for .NET software engineers.

    Learn More
    From Zero to Hero: Testing with xUnit in C#
    course

    From Zero to Hero: Testing with xUnit in C#

    Learn how to test any codebase in .NET with the latest version of xUnit, the industry-standard testing library.

    Learn More
    Create a ChatGPT Console AI Chatbot in C#
    blog

    Create a ChatGPT Console AI Chatbot in C#

    This walkthrough is your hands-on entry point to create a basic C# console application that talks to ChatGPT using the OpenAI API.

    Learn More