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
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.
Make it yours · 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.
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.csat start-up
DueDateServicescoped: one per request
HolidayCalendarsingleton: one for the app
new (one for the app)
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 repoExample 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
Containerbuilds and keeps
1/9A lifetime answers one question: how long does the keep an object it built?Request 1 arrives. Its code the container for each service twice.Transient: two asks, two new objects. Look at the two different addresses.Scoped, first ask: the container builds one and remembers it for this request.Scoped, second ask: nothing new is built. The container hands back the same 0x3C.Singleton: built once, the first time anyone in the app asks for it.Request 1 ends. Its the transient and scoped objects. Singleton stays.Request 2 gets new transient and scoped objects, and the same singleton as before.Transient: one per ask. Scoped: one per request. Singleton: one per app.
In plain words
A class gets the it needs through its . The decides: build a new object, or hand back one it already has? The lifetime, set when you it, is that choice.
Transient is a paper cup: a new one every time.
Scoped lives for one and is thrown away when the request ends, so the next request starts clean.
Singleton is the office kettle: one for everyone, all day. If two people use it at the same moment, it must still work. That is .
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.
Requestscope A, then B
PaymentValidatortransient
ServicingDbContextscoped
TimeProvidersingleton
scope A opens
1/9Scoped. 10:02: a customer pays. ASP.NET Core opens a for this request.Transient. The controller needs a . The container builds a new one: #1.Scoped. It also needs . The scope builds one and remembers it: context A., built for the same request, asks too: validator #2, but the same context A.Singleton. The service asks for the date. One for the whole app. on context A writes the payment and the new balance in one transaction.Scoped ends. Response sent, scope disposed: context A too. stays.Request B, one second later: validators #3 and #4, a new context B, and the same .Recap across both requests: 4 validators, 2 contexts, 1 .
The story
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.
LoansController
LoanRepository
AuditWriter
1/8Before ASP.NET Core, nothing built objects for you. A class that needed something used .The repository needs the database, so it opens its own connection.The audit writer opens another. One request, two connections, so no shared transaction.A common patch back then: one connection that every class shares.Two requests use it . A connection runs one command at a time, so the second throws.The real problem: each class called itself, so each class decided how long its parts lived.Now the builds: a class lists its needs in its and gets them.How long each object lives is now one word at .
Why it had to change
Lifetimes were scattered across every class that called , and nobody could see or change them in one place.
How it works now
One word at now decides it: , or .
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.
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.
Request 1AA
Request 2BB
Request 3CC
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
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.
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.
ReportedInside one request, what is the difference between transient and scoped?
Scoped: every class in the request gets the same object. Transient: every class gets its own. It matters when the object holds state, like a with unsaved changes: with transient, two repositories cannot save in one transaction.
Two questions. Does it hold this request's data? Scoped. Is it shared on purpose or costly to build, and thread-safe? Singleton, if it uses nothing scoped. Otherwise transient. Then I check it never depends on something that lives shorter than itself.
What happens if a singleton depends on a scoped service?
The scoped object is built once and stays inside the singleton for the life of the app. That is a captive dependency. With a it means a change tracker that only grows, stale reads and, when requests share it, concurrency errors.
Answer, then open the next level. That is the order an interviewer digs in.
Reporteda candidate was asked Intervieweran interviewer asks it
ReportedInside one request, what is the difference between transient and scoped?
Scoped: every class in the request gets the same object. Transient: every class gets its own. It matters when the object holds state, like a with unsaved changes: with transient, two repositories cannot save in one transaction.
LoanRepository
LedgerRepository
1/6Recording a payment changes two tables. Two repositories do the work.Scoped: both repositories get the same DbContext.One SaveChanges writes both in one transaction. Both succeed, or both .Transient: each repository gets its own DbContext.Two saves, two transactions. The loan saves. The ledger write fails.The payment shows on the loan with no ledger entry. Nothing rolls it back.
ReportedGive a real scenario where you used transient.
Typed HTTP clients and validators. AddHttpClient<ICreditBureauClient, FakeCreditBureauClient>() registers the client as transient while pools the connections underneath. Validators keep nothing, so a new one per request is free. I avoid transient for anything that owns a connection itself. Why the factory: new HttpClient() per call runs out of sockets under load, and one static client never sees DNS changes.
A console app or background job has no HTTP request. What is a scope there?
A scope is a box the container opens and closes. ASP.NET Core opens one per request for you. Anywhere else you open it yourself, and use when services dispose asynchronously. The same goes for Program.cs when you migrate or seed: open a scope from app.Services. Resolving from app.Services directly throws "Cannot resolve scoped service from root provider" in Development, and in Production quietly keeps one context for the app's life.
awaitusingvar scope =
scopes.CreateAsyncScope();
var db = scope.ServiceProvider
.GetRequiredService<ServicingDbContext>();
ReportedHow do you decide which lifetime to use?
Two questions. Does it hold this request's data? Scoped. Is it shared on purpose or costly to build, and thread-safe? Singleton, if it uses nothing scoped. Otherwise transient. Then I check it never depends on something that lives shorter than itself.
InterviewerWhy not…Why not register everything as transient to be safe?
It is not safe. Two repositories in one request would each get their own , so they cannot share a transaction, and costly objects get rebuilt on every ask. One r/dotnet interviewer admits they once registered everything as transient.
No. The container promises one instance, nothing more. Every request thread calls it at the same time, so its state must be immutable or use thread-safe types.
Your singleton keeps rates in a . How do you make it safe?
or , or build an immutable copy and swap it in with one assignment. A plain written by two threads corrupts itself.
The container disposes what it built, if it implements or .Transient and scoped objects go when their scope ends; singletons at shutdown. Anything you build with yourself, you dispose. Two traps: an instance you pass in yourself (AddSingleton(new RateCache())) is never disposed by the container, but one built by a factory lambda is. And a service that is only throws if its scope is disposed synchronously, which is why exists.
// Built by the container: it disposes it.public LoanRepository(ServicingDbContext db)
// Built by you: you dispose it.usingvar db = newServicingDbContext(o);
A disposable transient is resolved from the root provider in a loop. What happens to memory?
It leaks. The root provider keeps every disposable it builds on a list, to dispose at shutdown, so the garbage collector cannot free any of them. Resolve inside a scope instead.
One class implements two interfaces, both registered as singleton. How many objects?
Two: AddSingleton<IA, Impl>() and AddSingleton<IB, Impl>() each build their own. Register the class once and forward the second: AddSingleton<IB>(sp => sp.GetRequiredService<Impl>()).
How do you swap a registration in an integration test?
In , with RemoveAll<T>() then your fake. Watch singletons: every test sharing one factory shares them, so their state leaks between tests.
Which lifetime does use, and why?
Scoped: registers it that way by default. One request is one , so every repository shares the same context and one . It is not thread-safe, so two requests must never share one.
Can you register as a singleton?
You can, and you should not. Every request would share one context.
Three things. Threading: two requests use it at once and EF Core throws. Memory: a keeps a list of every row it loaded (the ) so can spot edits; a scoped one is thrown away after the request, but a captive one never is, so the list grows with every request. Stale data: when it loads a row it already has, it hands back its old copy, so a rate changed in the database is not seen.
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed.
What could happen if is registered as transient?
Each class gets its own context. Changes made through one are invisible to the other, and there is no shared transaction, so one save can succeed while the other fails. The first follow-up's animation shows a half-saved payment.
A needs the database, and it is a singleton. What do you do?
Inject . Open a scope for each run of the loop, resolve the context from it, and let the scope dispose it. Microsoft's docs show the same pattern.
while (!ct.IsCancellationRequested)
{
awaitusing (var scope =
_scopes.CreateAsyncScope())
{
var db = scope.ServiceProvider
.GetRequiredService<
ServicingDbContext>();
await DispatchAsync(db, ct);
} // scope closed before the waitawait Task.Delay(_every, ct);
}
Why not…Why not inject everywhere and resolve what you need?
That is the service locator pattern: asking for things by hand instead of listing them in the constructor. The constructor no longer shows what the class needs, a missing registration fails at run time instead of start-up, and tests must build a container. I resolve by hand only at the edges, like this loop.
What is , and when would you use it instead?
A singleton factory that builds a context when you ask. I use it when one scope is the wrong size: Blazor Server, parallel work inside one request, or a singleton that needs short-lived contexts. I own the dispose.
builder.Services
.AddDbContextFactory<ServicingDbContext>();
awaitusingvar db =
await _factory.CreateDbContextAsync(ct);
What does change?
It reuses context objects across requests and resets them in between; each request still has its own while it runs. So never keep per-request state, such as a tenant or dealer id, in the context's constructor: the next request gets the old value, which leaks one client's data to another.
What happens if a singleton depends on a scoped service?
The scoped object is built once and stays inside the singleton for the life of the app. That is a captive dependency. With a it means a change tracker that only grows, stale reads and, when requests share it, concurrency errors.
Does it behave the same in Development and Production?
No. In Development the host turns on and , so the app fails at start-up. In Production both are off by default, so the app starts and fails later under load. The lending API turns both on everywhere.
Development:
$ dotnet run
Unhandled exception. System.AggregateException: Some services are not able to be constructed ... Cannot consume scoped service 'Lending.Infrastructure.Origination.OriginationDbContext' from singleton 'Lending.Infrastructure.Origination.RateCache'.
Production, checks off, two requests overlap:
fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed.
at Lending.Infrastructure.Origination.RateCache.GetAsync(String product)
What do and do? Would you turn them on in production?
throws when a scoped service is resolved from the root or held by a singleton; on its own it fails at resolve time. checks at start-up that every registration can be built, by walking each constructor's needs; it creates nothing and cannot see inside factory lambdas. Together they fail start-up. costs some start-up time, a little on each resolve, which is why Microsoft leaves them on only in Development. I turn them on everywhere, or at least in a test that builds the host as Production.
InterviewerHow do you avoid pinning a transient resource to a singleton?
Three ways. Make the consumer shorter-lived too. Or keep it a singleton and inject a factory (,) and ask it per call. Or pass the object in as a method argument instead of storing it in a field.
// The caller passes its own context in;// the singleton never stores it.publicTask<decimal> GetAsync(
string product, OriginationDbContext db)
=> db.ProductRates
.Where(r => r.Product == product)
.Select(r => r.AnnualRate)
.FirstAsync();
Scoped depends on transient. Singleton depends on transient. Which is a problem?
Scoped on transient is fine: the transient lives for the request. Singleton on transient pins the transient for the life of the app. That is harmless if it keeps nothing, and a bug if it holds a connection or a typed . does not catch this one.
// Both take a transient service.AddScoped<LoanService>(); // fineAddSingleton<RateCache>(); // pinned
Which options interface can a singleton take?
is a singleton, read once at start. is scoped and re-read per request, so a singleton cannot take it (the same scope error). is a singleton with and : the one a singleton uses for settings reloaded from .
Production bugs, and how to tell them
Each one: what you see, why, the fix in code, and the line to say.
Captive dependency: a singleton holds a DbContext
$ dotnet run
Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: RateCache Lifetime: Singleton ImplementationType: RateCache': Cannot consume scoped service 'OriginationDbContext' from singleton 'RateCache'.)
(trimmed)
Cause
A singleton took the scoped in its constructor and kept it in a field.
Fix
Inject and open a scope per call, or make scoped and keep the shared copy in .
Diagram · step 1 registers the context as scoped.
Container
OriginationDbContextregistered: scoped
Program.cs · line 3 · 1 of 2The context is scoped. Why: one request is one unit of work.
Say it
The scoped object was built once and lived as long as the singleton. With a that meant a growing change tracker, stale reads, and concurrency errors under load. We fixed it with , and we keep and on in every environment so it fails at start-up.
One customer sees another's loans
No exception. A customer reports seeing someone else's loan list.
It happens about once in 2,000 requests, only under load.
Nobody can reproduce it locally.
Cause
, which holds the caller's id, was registered as a singleton. Two overlapping requests wrote into the same object.
Fix
Register it as scoped. Then add a test that two parallel requests for two users each get only their own loans.
Diagram · step 1RequestContext holds who is calling. Someone registered it as a singleton: one object for all.
RequestContextsingleton (the bug)
UserId = (empty)
Before · Program.cs · line 2 · 1 of 1The bug: one for every request. It holds per-request data, so it must be scoped.
Say it
A singleton is shared by every request, so any per-request state in it leaks between users. It only shows when requests overlap, which is why tests passed. Per-request data is scoped, always.
A singleton with a plain Dictionary
System.InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.
at System.Collections.Generic.Dictionary`2.TryInsert(...)
at Lending.Infrastructure.Origination.RateCache.GetAsync(String p)
Cause
Two request threads wrote to a plain inside a singleton at the same moment.
Fix
or , or build an immutable copy and swap it in with one assignment.
Diagram · step 1One RateCache for the whole app. Every request thread uses its one Dictionary.
RateCachesingleton
Dictionaryplain, not thread-safe
Before · RateCache.cs · line 2 · 1 of 4A plain inside a singleton. Many readers at once are fine, but one writer while anyone else reads or writes is not.
Say it
The container guarantees one instance, not thread safety. Every request thread calls a singleton at once, so its state must be immutable or use thread-safe types. A plain written from two threads may throw, silently lose writes, or hang a thread at 100% CPU.
Two queries at once on one DbContext
System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext.
Cause
ran two queries on the request's single to save time.
Fix
Await them one after the other. If they really must run in parallel, give each its own context from .
Diagram · step 1One request, one scoped context. A context runs one query at a time.
ServicingDbContextscoped: one per request
idle
SQL Server
Before · LoanRepository.cs · line 1 · 1 of 4Called once per request. _db is that request's scoped context: one connection, one query at a time.
Say it
Scoped means one per request, not one per task. A still does one operation at a time, even inside one request.
Memory climbs for days, a restart "fixes" it
No exception. Memory grows a little every hour.
After a restart it drops, then starts climbing again.
Cause
A singleton loop (a hosted service: it runs for the app's whole life) asked the root provider for a disposable transient, once per reminder. The root keeps every disposable it builds until shutdown.
Fix
Open a scope per unit of work and resolve inside it. Or, if the gateway client is thread-safe, register it as a singleton and build it once.
Diagram · step 4Now a singleton loop asks the instead. No scope is open.
SmsGatewayholds a connection
Before · ReminderSender.cs · line 4 · 1 of 3This is the root provider: a hosted service is built from the root, not from a request's scope.
Say it
The container disposes what it builds when the scope that built it ends. From the root provider, that is shutdown, so a loop that resolves disposables from the root leaks. A memory dump shows thousands of them held by the root provider's dispose list.
Production incident: Paid customers got past-due texts
P2 · serious, has a workaroundBMW FSlatent: hidden for weeksIntegration hub workers
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
dotnet new worker -n CaptiveLab, then add a class registered with and take it in the 's constructor.
dotnet run: the launch profile sets Development, so it stops at start-up with "Cannot consume scoped service".
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.)
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."
SourcesSourced: 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
Don't sayScoped means one per user or per session.SayOne per scope. In a web app the scope is one HTTP request; the same user's next request gets a new one.
Don't saySingletons are thread-safe.SayA singleton is one object shared by all threads, so I have to make it thread-safe.
Don't sayTransient is always the safe choice.SayTransient can break a unit of work: two objects, two transactions. It also rebuilds costly objects.
Don't sayScoped is only for DbContext.SayScoped is for any per-request state: the , the current user, a request's unit of work.
Don't sayA static field is the same as a singleton.SayA field is a singleton the container cannot see: no test swaps, no dispose, no start-up checks.
Traps
What should you say instead? Tap to check.
Don't sayScoped means one per user or per session.
Say instead
One per scope. In a web app the scope is one HTTP request; the same user's next request gets a new one.
Don't saySingletons are thread-safe.
Say instead
A singleton is one object shared by all threads, so I have to make it thread-safe.
Don't sayTransient is always the safe choice.
Say instead
Transient can break a unit of work: two objects, two transactions. It also rebuilds costly objects.
Don't sayScoped is only for DbContext.
Say instead
Scoped is for any per-request state: the , the current user, a request's unit of work.
Don't sayA static field is the same as a singleton.
Say instead
A field is a singleton the container cannot see: no test swaps, no dispose, no start-up checks.
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.
Middleware is a class with a constructor that takes RequestDelegate next, and an InvokeAsync(HttpContext) method.
Where does the count live? It has to outlive every request.
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.
Give the class a Guid Id set once, in its constructor or initializer. Same id means same object.
Ask for all three in the endpoint and again inside a second service. Compare the ids inside one request, then across two.
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.