← Home
.NETDependency injectionReported by candidates

What are the service lifetimes in ASP.NET Core, and how do you choose one?

Asked at Neudesic, Honeywell, HCL, Wipro, Infosys, PwC and Capgemini, and by .NET interviewers on r/dotnet.

Say this first · about 30 seconds
Make it yours · UDC Finance due dates (invented example)
The code, simplified

Invented to show the shape, in C#. Tap Next to see who is built when, for one request.

UDC.sln · where these 3 files sit
  • src/
    • Api/
    • Application/Payments/
Diagram · step 1At start-up, Program.cs builds one with two holidays: a singleton.
Program.cs · line 4 · 1 of 3Makes the list of NZ holidays. Christmas 2026 is a Friday; the bank's calendar treats Monday 28 December as the Boxing Day holiday.

Fix later in your repo Example written for this card: invented, not from a real codebase.

Say it out loud, then check

Show the 30-second answer
  • A lifetime tells the how long to keep an object it built, and so who shares it.
  • Transient: you get a new object every time you ask for one.
  • Scoped: one object per HTTP request. Every class in that request gets the same one, like the .
  • Singleton: one object for the whole app, shared by every request at once, so it has to be .
  • How I pick: holds this request's data, scoped. Shared or costly to build, and thread-safe, singleton. Anything else, transient.
  • One rule: never inject something shorter-lived into something longer-lived. That is a captive dependency.
Check your answer: tick what you said

You said 0 of 8

Then add your example: UDC Finance due dates (invented example)
  • Situation: at UDC Finance in New Zealand, a payment due on a weekend or holiday moves to the next working day, and the customer portal shows that date. A teammate wrote and .
  • What happened: was registered as a singleton, but it takes the scoped : a captive dependency. In Development, .NET's start-up check () stopped the app; Production has it off, so the bug ships silently.
  • What I did: I made scoped. stays a singleton: it only reads its list, so requests share it.
  • What I'd say: this request's data, scoped; shared and read-only, singleton. A longer-lived service must not capture a shorter-lived one.

See it

1/9A lifetime answers one question: how long does the keep an object it built?

In plain words

The senior answer · about 2 minutes
What they are
  • There are three lifetimes, and you pick one when you a service.
  • Transient means the container builds a new object every time something asks for it.
  • Scoped means one object per scope. In ASP.NET Core every HTTP request gets its own scope, so in practice it's one object per request, and it's disposed when the response is done.
  • Singleton means one object for the whole app. The builds it once and keeps it until shutdown.
How I choose
  • I choose by state, not by habit.
  • If the class keeps no state, like a validator, I make it transient. A typed is transient too, and I never hold one inside a singleton.
  • If it holds state for one request, I make it scoped. is the classic case: one request is one , so one is one transaction. , which reads the caller's token, is scoped for the same reason.
  • If it's meant to be shared, or it's expensive to build, I make it a singleton: , our , , .
  • A singleton is used by many at the same time, so I keep its state read-only, or I use or .
The rule
  • A service should only depend on things that live as long as it does, or longer. I check this in code review.
  • If a singleton takes a scoped , it keeps that one context forever. That's called a captive dependency.
  • The context's change tracker, the list of rows it has loaded, keeps growing, reads go stale, and when two requests use it at once EF Core throws.
  • So when a singleton or a hosted service needs scoped work, it takes and opens a new scope for each unit of work.
In production
  • I turn on and in every environment, not just Development. Then these mistakes fail at start-up in instead of under load in production.
  • The container also what it builds. If you resolve disposable transients from the root provider in a loop, they're never released, and memory grows until the app restarts.

All three in one payment

One instalment payment at BMW FS, in time order. Each step names the lifetime at work.

1/9Scoped. 10:02: a customer pays. ASP.NET Core opens a for this request.

The story

  1. Before

    .NET Framework had no container built in. A class that needed something built it itself with inside its . Teams that wanted a container added Autofac, Unity or Ninject, each with its own words for lifetimes.

    1/8Before ASP.NET Core, nothing built objects for you. A class that needed something used .

  2. Why it had to change

    Lifetimes were scattered across every class that called , and nobody could see or change them in one place.

  3. How it works now

    One word at now decides it: , or .

  4. What happens next

    Each request gets a . Scoped objects are shared inside it and when it ends; singletons wait in the root provider until the app stops. If you switch the checks on, the container also checks the lifetime rules at start-up.

  5. What we gained

    One place to read and change lifetimes. One per request, so one is one transaction. No hand-written clean-up. And classes you can test by passing fakes into the constructor.

In the lending API

Program.cs runs once at start-up. Each line tells the what to build and how long to keep it: that is the lifetime.

Only the key lines are shown; Deep walks the whole file.

Lending.sln · where this file sits
  • src/Api/
⋯ 3 lines in Deep⋯ 1 line in Deep⋯ 4 lines in Deep⋯ 1 line in Deep
Program.cs · line 19 · 1 of 4: one object for the whole app. India's rules never change per request, so one shared copy is safe.

The code, file by file

Slice 3 of the lending API: all three lifetimes in one Program.cs, then the classes that use them. Each note says why that lifetime was picked.

Lending.sln · where these 4 files sit
  • src/
    • Api/
      • Services/
    • Application/Servicing/Loans/
    • Infrastructure/Servicing/
Program.cs · line 1 · 1 of 14One Program.cs for the whole API. In the repo these lines sit in AddApplication() and AddInfrastructure(); they are shown together here.

Which one, and what lives where

Two questions pick the lifetime: what does the object hold, and who should share it? Tap an answer.

Scoped

Scoped means one per . ASP.NET Core opens one scope per HTTP request, so it is one object per request.

Everything in one request shares it and its ; the next request starts clean. Not only : anything holding this request's data, like who is calling.

6 asks, 3 objects. Same letter means the same object.

You will see it on

· repositories and services that use it · : current user / tenant · · per-request correlation or audit context

Lives here in the lending API
  • ServicingDbContext
    builder.Services.AddDbContext<
      ServicingDbContext>(o =>
        o.UseSqlServer(cs));

    registers it as scoped by default. One request is one unit of work: every repository shares one context, and one writes all their changes in one transaction. It is not thread-safe, so two requests must never share it.

  • CurrentUser
    builder.Services.AddScoped<
      ICurrentUser, CurrentUser>();

    Holds who is calling, read from this request's token. Per request by nature.

  • LoanService
    builder.Services.AddScoped<
      ILoanService, LoanService>();

    Uses the scoped context and user, so it must be scoped too. A service cannot safely outlive what it holds.

Registered as a singleton by mistake, it hands one request's data to the next user.

What they ask next

The follow-ups interviews really ask. Answer out loud, then tap to check.

Follow-up tree

Answer, then open the next level. That is the order an interviewer digs in.

Reporteda candidate was asked Intervieweran interviewer asks it

Production bugs, and how to tell them

Each one: what you see, why, the fix in code, and the line to say.

Production incident: Paid customers got past-due texts

P2 · serious, has a workaroundBMW FSlatent: hidden for weeksIntegration hub workers

Composed, not a real event at BMW FS. Every mechanism is real; the stakes are modelled on a real case: CFPB order against Hyundai Capital America (2022)

The story in 5 lines
  1. BMW FS customers who had paid got "payment past due" texts, and wrong days-past-due values were headed for the monthly file.
  2. Service desk tickets reached our . showed no errors, because nothing threw.
  3. The hub's is registered with , so it is a singleton. It held one scoped for weeks and read payments from its own stale memory.
  4. We switched the texts off with a and held the file, then shipped one fresh scope per run plus a data-fix script.
  5. and are now on in every environment, and a test starts the app as Production.
Who's who (6 people)
You
.NET developer on the vendor team (L3). Writes the fix and the data script.
Tech lead
Vendor senior. Runs the bridge call, reviews every change.
L2 pod
Vendor support in Navi Mumbai. First look, runbooks, flips flags.
BMW FS IT manager
Approves changes, including the fast-track ones.
BMW FS DBA
The only person who runs SQL in production.
BMW FS compliance
Owns the credit bureau file and what customers are told.

Tap a step. The map lights the place where it happens.

1/8 Detect

Paid customers got past-due texts. Their calls reach BMW FS's desk, then our .

Mon 29 Sep (US) → Tue 30 Sep, 09:30 ISTBMW FS desk, then L2

  • BMW FS customers call: "I paid on Friday, why am I texted that I'm late?" The () logs three tickets during the US day; in India it is night.
  • At 09:30 IST the sees them, finds no errors anywhere and pulls you in: it is our code, not FIS's, because the texts come from our hub.
  • No alert fired. The had shipped nearly three weeks earlier, on 11 Sep. It runs nightly at 02:00 ET on one hub-workers instance with .
The third ticket (ServiceNow)
INC0048213 · BMW FS Service Desk
Customer paid 26 Sep (ACH, posted in FIS).
Got "payment past due" text 29 Sep.
Third ticket like this today.
In theory

A symptom alert finds it first, e.g. "a past-due text went to an account with a payment in the last 3 days".

In practice

Customers found it, nearly three weeks in. Nothing threw an exception, so no error alert could fire.

Try it yourself · 15 minutes, on your Mac
  1. dotnet new worker -n CaptiveLab, then add a class registered with and take it in the 's constructor.
  2. dotnet run: the launch profile sets Development, so it stops at start-up with "Cannot consume scoped service".
  3. DOTNET_ENVIRONMENT=Production dotnet run --no-launch-profile: it starts. That is the trap in this incident. (Without --no-launch-profile the profile wins and you are still in Development.)
  4. Fix it with and , and run both again.
You should see

Development fails at start-up; Production starts with the bug inside; after the fix both start.

The senior move

"When a restart fixes it, I treat that as state living too long, not as a fix. And I turn the scope checks on in every environment, because Production skips them."

Sources Sourced: the DI error text (dotnet/runtime), the checks being Development-only (), scope-per-run guidance (Microsoft DI guidelines), tracked entities not refreshed (EF Core docs), the behaviour, and the Hyundai order for the stakes. Composed: BMW FS's systems, every date, number and name, and the (standard columns).

Traps

Traps

What should you say instead? Tap to check.

Live coding

Easy first, then harder. Set a timer and type it before you look.

Task 1

Reportedeasy · 15 minutes

Write a middleware class that counts every request made to the backend. Then say what lifetime the counter needs, and why.

Reported on r/dotnet: intermediate .NET role, got the job (May 2025)
Stuck? Hints
  1. Middleware is a class with a constructor that takes RequestDelegate next, and an InvokeAsync(HttpContext) method.
  2. Where does the count live? It has to outlive every request.
  3. Many requests run at the same moment. What happens to count++ then?
Show a solution
Lending.sln · where these 3 files sit
  • src/Api/
    • Middleware/
RequestCounter.cs · line 12 · 1 of 1Many requests run at once on many . _count++ is read, add, write: two threads can read the same value and one update is lost. does the read, add and write as one step no other thread can split.
What the interviewer checks
  • The counter is a singleton (one count for the app), and you can say why.
  • Thread-safe increment: , not count++.
  • await _next(context) is called, so the request carries on.
  • Registered with app.UseMiddleware<...>(); where it sits in the pipeline decides what is counted.
  • Senior extra: the count is per running copy of the app. With 3 replicas (copies), count with an OpenTelemetry metric or Redis instead.
  • Senior extra: ASP.NET Core 8+ already records http.server.request.duration, which includes a request count. In production you read that metric instead of writing a counter.

Task 2

Question index: Mustmedium · 25 minutes

Register one class as transient, scoped and singleton. Print each object's id from an endpoint and from a second service, for two requests. Then break it: make the second service a singleton and read the error. Fix it.

Question index (Must, medium); the same exercise as Microsoft Learn's DI tutorial
Stuck? Hints
  1. Give the class a Guid Id set once, in its constructor or initializer. Same id means same object.
  2. Ask for all three in the endpoint and again inside a second service. Compare the ids inside one request, then across two.
  3. Make the second service a singleton while it still takes the scoped one. Run in Development and read the start-up error.
Show a solution
Operation.cs · line 13 · 1 of 1Set once, when the container builds the object. Same id means same object; that is all the test needs.
What the interviewer checks
  • Transient: a different id at every ask, even inside one request.
  • Scoped: the same id everywhere inside one request, a new one in the next request.
  • Singleton: one id for the whole app.
  • The break fails at start-up in Development ( and ), and you can read the message: cannot consume a scoped service from a singleton.
  • Fix: keep the second service scoped. If it must be a singleton, it takes and opens a scope per call.
  • Senior extra: these checks are on only in Development by default. Turn them on in every environment so a wrong registration fails the deploy, not a customer.
With AI tools

Interviewers now ask how you check AI-written code. A story that lands: Copilot suggested registering the as a singleton to "fix" an . I rejected it, because is not and must be scoped. The real bug was a background service holding a scoped service; I fixed it with and added a test that builds the host with . Lifetime bugs pass a quick review and fail only under load, so AI-written registrations are where I slow down.

Glossary: every name, one line each →

My notes

Paste the NOTES block from your doubt chat here. The grader never reads notes; the coach reads "Still unclear".

Read moreWhere it was asked