Skip to content

Expert · Web APIs with ASP.NET Core and EF Core · topic 33 of 43 · 30 min

Dependency injection and service lifetimes

Explanation

Dependency injection (DI) means a class receives the objects it needs (its dependencies) from outside, usually through its constructor, instead of creating them itself. ASP.NET Core has a built-in DI container: you register services in Program.cs with AddSingleton, AddScoped or AddTransient, and the framework creates and passes them where needed. The lifetime decides how long each instance lives.

The problem DI solves

If OrderService writes var sms = new SmsSender(); inside itself, it is welded to that one class. You cannot switch providers, and in tests you cannot replace it with a fake that does not send real SMS. With DI, OrderService just says "I need an ISmsSender" in its constructor, and whoever creates OrderService decides which one to give. You already saw this idea with interfaces in module 5.

Think of it like this: DI is like a hospital operation theatre. The surgeon does not go out to buy scalpels and gloves before each operation. The hospital (the DI container) prepares and hands over exactly what the surgeon needs. The surgeon only says what is needed, not where it comes from.

Registering and receiving services

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<IPriceCalculator, GstPriceCalculator>();
builder.Services.AddScoped<IOrderService, OrderService>();

var app = builder.Build();

app.MapPost("/orders", (CreateOrder req, IOrderService orders) =>
    Results.Ok(orders.Place(req)));

app.Run();

public interface IPriceCalculator { decimal WithTax(decimal amount); }
public class GstPriceCalculator : IPriceCalculator
{
    public decimal WithTax(decimal amount) => amount * 1.18m;
}

public interface IOrderService { object Place(CreateOrder req); }
public class OrderService : IOrderService
{
    private readonly IPriceCalculator _calc;
    public OrderService(IPriceCalculator calc) => _calc = calc;   // constructor injection
    public object Place(CreateOrder req) => new { req.Item, Total = _calc.WithTax(req.Amount) };
}

public record CreateOrder(string Item, decimal Amount);
The container sees that OrderService needs an IPriceCalculator, creates one, and passes it in. Minimal API handlers receive services simply as parameters.

The three lifetimes

LifetimeOne instance per...Typical use
AddSingletonWhole application, for as long as it runsStateless helpers, caches, configuration objects; must be thread-safe
AddScopedHTTP request (shared within that request)DbContext, unit-of-work, per-request services
AddTransientEvery time it is asked forLight, stateless services
csharp
builder.Services.AddSingleton<Counter>();
builder.Services.AddScoped<RequestInfo>();
builder.Services.AddTransient<Stamp>();

public class Counter { private int _n; public int Next() => Interlocked.Increment(ref _n); }
public class RequestInfo { public Guid Id { get; } = Guid.NewGuid(); }
public class Stamp { public DateTime Created { get; } = DateTime.UtcNow; }
Counter keeps counting across all requests. Each request gets its own RequestInfo id. Every injection of Stamp is new. Interlocked.Increment makes the counter safe when many requests run at once.

The captive dependency trap

A service must not depend on a service with a SHORTER lifetime. If a singleton takes a scoped DbContext in its constructor, that one DbContext is captured and reused by every request forever, which causes wrong data and threading errors. In the Development environment, ASP.NET Core checks this and throws an error at startup saying it cannot consume a scoped service from a singleton. Fix it by making the outer service scoped too.

Outer serviceMay depend on
SingletonSingleton only
ScopedScoped or Singleton
TransientAnything

Controllers and primary constructors

csharp
[ApiController]
[Route("api/orders")]
public class OrdersController(IOrderService orders, ILogger<OrdersController> logger) : ControllerBase
{
    [HttpPost]
    public IActionResult Place(CreateOrder req)
    {
        logger.LogInformation("Placing order for {Item}", req.Item);
        return Ok(orders.Place(req));
    }
}
Controllers get services through their constructor. A C# 12 primary constructor keeps it short. ILogger<T> is registered by the framework for you.

Common mistakes and fixes

  • "Unable to resolve service for type IOrderService": you forgot to register it. Add builder.Services.AddScoped<IOrderService, OrderService>();.
  • Registering after builder.Build(): registrations must happen before Build.
  • Using new for services inside other services: you lose DI benefits. Ask for them in the constructor.
  • Storing per-user data in a singleton: all users share it. Use scoped services or a database.
  • Huge constructors with ten dependencies: a sign the class does too much. Split it.
Tip: Built-in services you will inject all the time: ILogger<T> (logging), IConfiguration and IOptions<T> (settings), IHttpClientFactory (after builder.Services.AddHttpClient()), and your DbContext.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Dependency injection and service lifetimes · 1/7

Dependency injection

In this lesson you will let ASP.NET Core create and hand out your services: dependency injection.

Prefer a video by a teacher? Find free videos on “ASP.NET Core dependency injection singleton scoped transient explained” on YouTube ↗

In real life: Swapping a real SMS gateway for a fake in development

A clinic booking API sends appointment SMS through a paid gateway. In development, nobody wants real SMS going to patients, so Program.cs registers a fake sender that just logs. Production registers the real one. The booking code is identical in both.

csharp
if (builder.Environment.IsDevelopment())
    builder.Services.AddSingleton<ISmsSender, LogOnlySmsSender>();
else
    builder.Services.AddSingleton<ISmsSender, GatewaySmsSender>();

builder.Services.AddScoped<IBookingService, BookingService>();

public interface ISmsSender { Task SendAsync(string phone, string text); }

public class LogOnlySmsSender(ILogger<LogOnlySmsSender> log) : ISmsSender
{
    public Task SendAsync(string phone, string text)
    {
        log.LogInformation("FAKE SMS to {Phone}: {Text}", phone, text);
        return Task.CompletedTask;
    }
}
GatewaySmsSender would call the provider's HTTP API. Because BookingService depends only on ISmsSender, nothing else changes.

Practice (0/4 done)

Task 1 · Question

Which lifetime gives one instance per HTTP request?

Task 2 · Question

A singleton service takes a scoped DbContext in its constructor. What is the problem?

Task 3 · Question

You get "Unable to resolve service for type IEmailSender". What is the most likely fix?

Task 4 · Practice

In a webapi project, create an IGreetingService with string Greet(string name), and two implementations: HindiGreeting ("Namaste, name") and EnglishGreeting ("Hello, name"). Register one in Program.cs, inject it into GET /greet/{name}, then switch the registration and confirm the endpoint changes without touching the endpoint code. Also register a singleton counter and return how many greetings were served.

Quick quiz

Question 1

1.What is dependency injection?

Question 2

2.Which lifetime is usual for an EF Core DbContext in ASP.NET Core?

Question 3

3.Why must singleton services be thread-safe?

Question 4

4.A main benefit of DI for testing is...

Question 5

5.Which can a scoped service safely depend on?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is dependency injection?

Answer: Giving a class its dependencies from outside instead of it creating them

Dependencies are supplied, usually via the constructor.

Which lifetime is usual for an EF Core DbContext in ASP.NET Core?

Answer: Scoped

AddDbContext registers it as scoped: one per request.

Why must singleton services be thread-safe?

Answer: Many requests use the same instance at the same time

One shared instance serves concurrent requests.

A main benefit of DI for testing is...

Answer: You can pass fake implementations of interfaces into the class under test

Fakes replace real email, SMS or database services.

Which can a scoped service safely depend on?

Answer: Scoped or singleton services

A dependency should live at least as long as the service using it.

Which injection style is recommended?Related

Answer: Constructor injection

Constructor injection gives final fields and easy tests.

From Spring and dependency injection

What does @Inject constructor do?Related

Answer: Tells Hilt how to create the class

Hilt calls it with the needed dependencies.

From Dependency injection with Hilt

const vs readonly: which can be set from a value calculated at run time in the constructor?Related

Answer: readonly

const must be a compile-time literal.

From The C# and .NET interview questions asked most often

Since which version does a new ASP.NET Core app start from Program.cs without Startup.cs?Related

Answer: .NET 6

The minimal hosting model arrived in .NET 6.

From ASP.NET Core: your first Web API

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Web APIs with ASP.NET Core and EF Core” before its exam: Configuration, secrets and the middleware pipeline, Entity Framework Core: databases with C#.
  2. Module 8 exam: Web APIs with ASP.NET Core and EF Core · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Testing, tooling and Unity. 43 topics left in the course before the final exam.

← Previous: Routing, model binding, validation and status codes