What is LINQ, and what is deferred execution? How do you page results with it?
Asked at Capgemini, Hexaware, ESQ, Krishiv and MaiVin, and reported again from TCS and Accenture rounds in September 2026.
Say this first · about 30 seconds
LINQ is a set of query methods in C#, like Where, Select, OrderBy and GroupBy. They work the same way on a list in memory or on a database table through EF Core, the library that turns them into SQL.
Deferred execution means building a query doesn't run it. It runs when you enumerate it: a foreach, ToList, Count or First.
With EF Core that's a feature: I chain Where, OrderBy, Skip and Take, and EF sends one SQL query with the filter and the paging inside it.
The traps: switching to in-memory LINQ too early pulls the whole table, and enumerating the same query twice runs it twice.
Make it yours · BMW FS payoff screen (invented example)
Situation: at BMW FS the screen showed a contract's last payment. The code used on the payments, with no order, so it returned whichever row SQL happened to give back.
What I did: I made the order explicit: by paid date, then , because a brand-new contract has no payments yet.
Result: the screen always shows the latest payment, and a new contract shows "no payments" instead of an error.
What I'd say: without on a database is a hidden bug: SQL doesn't promise any order unless you ask.
The code, simplified
Invented to show the shape, in C#. Tap Next to walk contract C-1042.
BMW.sln · where this file sits
Payoff/
Diagram · step 1Builds the query: payments for C-1042. Nothing runs yet.
LastPaymentQuery
Azure SQLPayments
Where(ContractId = C-1042)
LastPaymentQuery.cs · line 1 · 1 of 4The controller calls this for contract C-1042.
Fix later in your repoNeeds a database index on (ContractId, PaidOn), a sorted lookup, if the payments table grows large.
Say it out loud, then check
Show the 30-second answer
LINQ is a set of query methods in C#, like Where, Select, OrderBy and GroupBy. They work the same way on a list in memory or on a database table through EF Core, the library that turns them into SQL.
Deferred execution means building a query doesn't run it. It runs when you enumerate it: a foreach, ToList, Count or First.
With EF Core that's a feature: I chain Where, OrderBy, Skip and Take, and EF sends one SQL query with the filter and the paging inside it.
The traps: switching to in-memory LINQ too early pulls the whole table, and enumerating the same query twice runs it twice.
Check your answer: tick what you said
You said 0 of 7
Then add your example: BMW FS payoff screen (invented example)
Situation: at BMW FS the screen showed a contract's last payment. The code used on the payments, with no order, so it returned whichever row SQL happened to give back.
What I did: I made the order explicit: by paid date, then , because a brand-new contract has no payments yet.
Result: the screen always shows the latest payment, and a new contract shows "no payments" instead of an error.
What I'd say: without on a database is a hidden bug: SQL doesn't promise any order unless you ask.
See it
Our code
QueryIQueryable
EF Core
Azure SQL20,000 contracts
SELECT * FROM Contracts
20,000 rows
1/6Before: the screen called first. All 20,000 rows came back, then C# filtered them.Fix: build the query first. adds a filter to the description. Nothing runs yet., and add the order and the page. Still nothing runs. enumerates. EF Core turns the whole chain into one SQL query.Trap: enumerate the same query twice, it runs twice. Two round trips.Materialise once with , then use the list as often as you like.
In plain words
Words first. A database stores the data (here );SQL is the language it understands. EF Core is the .NET library that lets C# talk to it: you write C# queries and it writes the SQL. A query is a question you ask a collection of data. Enumerate means walking through the results one by one, as a does. To materialise a query means running it and keeping the results in a list. A round trip is one request to the database and its answer. An extension method is a method you call with a dot on a type that didn't define it; LINQ's is one.
LINQ is a way of asking questions of data in C#: "the contracts more than 30 days late, oldest first, 20 at a time". You write it as a chain of steps: , then , then and .
Deferred execution is writing a shopping list versus doing the shopping. Writing "milk, bread" doesn't buy anything. The shop trip happens when you go. With LINQ, the chain is the list; or a is the trip.
That's why the order of steps matters with a database. Hand the whole list to the shop (EF Core turns it into SQL) and it brings back only what you need. Carry the entire shop home first ( before ) and then pick the items out at home, and you've carried everything.
The senior answer · about 2 minutes
What LINQ is
LINQ, Language Integrated Query, is a set of methods like ,,, and that you chain to describe what you want. Each takes a lambda, a small inline method; the explains those. The same code reads a list in memory or, through EF Core, a SQL table.
The difference is the type you start from. A is an : LINQ runs in .NET, item by item. EF Core's is an : LINQ doesn't run at all, it builds an expression tree, a description of the query kept as data, which EF reads and translates into one SQL statement.
Deferred execution
A LINQ query is a recipe, not the meal. and return a query object immediately and touch no data. The work happens when something enumerates it: a ,,,,.
That's what makes paging cheap with EF Core: I add ,, and , and only sends one SQL query with the filter, order and offset in it. Twenty rows come back, not twenty thousand.
It's also the source of two classic bugs. Enumerate the same query twice and it runs twice, two database round trips or two passes over a big list. And it reads the data as it is when it runs, not when you wrote it, so a list changed in between gives a different answer.
Where it goes wrong in production
The expensive one: a method EF can't translate, so someone adds or before the to make the error go away. It works, and it now loads the whole table into memory to filter it. That's the incident on this card.
I check the SQL a query really sends, with ToQueryString() or EF's logging in development, and I keep translatable filters first, then switch to memory only for the small remainder.
vs is about intent: says "there must be exactly one" and throws if the data breaks that rule, so it catches duplicates. It asks the database for two rows to check. On a unique key I use ; for "the latest payment" I use with .
All in one flow
Coming Not written yet: the card pass adds it.
The story
Before
UDC Finance's collections team works through overdue contracts every morning on a screen that shows 20 at a time. The first version loaded all contracts with , then picked the overdue ones and the page in C#.
Why it had to change
That was fine with 2,000 contracts in test. In production there were 20,000, so every page click pulled 20,000 rows across the network to show 20. The screen took seconds, and memory jumped on each click.
How it works now
LINQ queries are deferred: ,, and only describe the query. Built on EF Core's , the whole description becomes one SQL statement when runs it. So the fix was to move to the end: filter, order and page in the database, bring back 20 rows. The diagram walks it.
What happens next
One query per page, 20 rows each. The same deferred behaviour has one trap left: enumerate a query twice and it runs twice, so the result is turned into a list once and reused.
What we gained
The database does the filtering it's built for, the API moves 20 rows instead of 20,000, and the code reads like the question the user asked.
In the lending API
The whole query is built first and runs once, at , as one SQL statement.
Only the key lines are shown; Deep walks the whole file.
Lending.sln · where this file sits
src/Application/Collections/
⋯ 1 line in Deep⋯ 4 lines in Deep⋯ 3 lines in Deep
OverdueQuery.cs · line 5 · 1 of 4_db is our EF Core database context; _db.Contracts is the contracts table. : a description EF Core will turn into SQL. No data yet.
The code, file by file
The UDC Finance collections screen in the lending API: overdue contracts, oldest first, a page at a time. Tap Next to walk the diagram with the code.
Lending.sln · where these 2 files sit
src/Application/
Collections/
Contracts/
Diagram · step 1Before: the screen called first. All 20,000 rows came back, then C# filtered them.
Our code
QueryIQueryable
EF Core
Azure SQL20,000 contracts
SELECT * FROM Contracts
20,000 rows
OverdueQuery.cs · line 1 · 1 of 11The collections controller calls it with the page number from the URL, for example page 2 of 20 rows.
What they ask next
The follow-ups interviews really ask. Answer out loud, then tap to check.
ReportedFirst, FirstOrDefault, Single, SingleOrDefault: what is the difference?
returns the first match and throws if there's none. expects exactly one and throws if there are none or more than one. and return null (or the type's default) when nothing matches, but still throws on two or more. Use on a unique key so duplicates fail loudly; use with an when you want "the latest" or "the top" one.
// 0 matches: First throws,// FirstOrDefault gives null// 2 matches: First returns one,// Single(OrDefault) throws
InterviewerIEnumerable vs IQueryable: what is the difference?
runs LINQ in memory, in .NET, over data you already have. builds an expression tree, a description of the query as data, that a provider like EF Core translates to SQL and runs in the database. Once a query becomes (with or ), every step after runs in memory, on everything loaded so far.
Answer, then open the next level. That is the order an interviewer digs in.
Reporteda candidate was asked Intervieweran interviewer asks it
ReportedFirst, FirstOrDefault, Single, SingleOrDefault: what is the difference?
returns the first match and throws if there's none. expects exactly one and throws if there are none or more than one. and return null (or the type's default) when nothing matches, but still throws on two or more. Use on a unique key so duplicates fail loudly; use with an when you want "the latest" or "the top" one.
// 0 matches: First throws,// FirstOrDefault gives null// 2 matches: First returns one,// Single(OrDefault) throws
InterviewerIEnumerable vs IQueryable: what is the difference?
runs LINQ in memory, in .NET, over data you already have. builds an expression tree, a description of the query as data, that a provider like EF Core translates to SQL and runs in the database. Once a query becomes (with or ), every step after runs in memory, on everything loaded so far.
ReportedHow does GroupBy work, and does it work with EF Core?
In memory, puts items with the same key into groups, each an with a and its items. With EF Core it translates to SQL GROUP BY when you follow it with an aggregate, like Select(g => new { g.Key, Total = g.Sum(p => p.Amount) }). A as the last step also works since EF Core 7, but it brings back every row and builds the groups in .NET; aggregate in the query to get a real SQL GROUP BY.
payments
.GroupBy(p => p.ContractId)
.Select(g => new {
g.Key,
Total = g.Sum(p => p.Amount) });
looks up by primary key and first checks EF's change tracker, the list of entities this request's database context has already loaded. If it's there, no database call. always queries the database, and can match on any condition.
InterviewerSkip and Take are slow on page 5,000. Why, and what do you do?
OFFSET still makes the database read and throw away every skipped row, so deep pages get slower. Keyset pagination fixes it: remember the last row's sort key and ask for rows after it, Where(c => c.Id > lastId).OrderBy(c => c.Id).Take(20), which an index (the database's sorted lookup on that column) answers directly. The cost: you can only go next or previous, not jump to page 5,000.
Each one: what you see, why, the fix in code, and the line to say.
Try first: predict how many overdue contracts it prints
var overdue = contracts
.Where(c => c.DaysOverdue > 30);
contracts.Add(new Contract("C-9", 45));
Console.WriteLine(overdue.Count());
// contracts held 2 overdue before the Add.
// Does it print 2 or 3? Decide, then open the answer.
Cause
It prints 3. didn't run when overdue was declared; it only stored the condition. enumerates the list as it is at that moment, which now includes C-9. That's deferred execution: the query reads the data when it runs, not when it's written.
Fix
If you want a snapshot, materialise it when you mean to: var overdue = contracts.Where(...).ToList(); runs the filter once, right there, and later changes to contracts don't affect it. And if you need the result twice, the also stops it running twice.
Before · Program.cs · line 7 · 1 of 2Stores the query. Nothing is filtered yet.
Say it
Deferred execution: the query is a description and runs at enumeration. Count ran after the Add, so it saw three. If I need a fixed result or I'll use it more than once, I call ToList at the point I mean, which also avoids running the query again on every enumeration.
Production incident: The arrears export loaded 1.8 million contracts to find 900
P2 · serious, has a workaroundUDC Financebad dataNightly arrears export (a .NET worker on Azure)
Composed, not a real event at UDC Finance. The mechanism is real EF Core: AsEnumerable before Where moves the filter into memory, and EF's own error message suggests it. Dates, counts, names, tickets and the query are composed.
The story in 5 lines
On Tue 13 Oct the nightly arrears export for UDC Finance ran out of memory and the restarted. The collections team had no call list at 07:00.
The week before, a data migration had loaded 1.6 million closed historical contracts into the same table. The table went from 200,000 rows to 1.8 million.
The export query had before its , added months earlier to get past EF Core's "could not be translated" error. So it loaded every contract into memory and filtered in C#.
Nothing alerted on growth: with 200,000 rows the waste was invisible. With 1.8 million, the hit its memory limit.
The fix put the filter back in SQL as an expression EF can translate. The export now reads about 900 rows, and a test checks the SQL has a WHERE.
Who's who (4 people)
You
.NET developer on the vendor team. Finds the cause, writes the fix and the test.
Tech lead
Vendor senior. Reviews the hotfix, runs the postmortem.
L2 pod
Vendor support. Sees the failed run, reruns the export.
UDC collections manager
Needs the arrears list at 07:00 NZ time for the morning calls.
Tap a step. The map lights the place where it happens.
Export job02:00 NZT
EF Core
Azure SQL1.8M contracts
Worker memory
export arrears
OutOfMemoryException
1/4 Detect
Detect: the export died at 02:41 with out-of-memory. No call list at 07:00.
Tue 13 Oct, 02:41 NZDT (Mon 12 Oct, 19:11 IST)The on-call L2 pod, then you
What: the arrears export crashed with at 02:41 and restarted. The file the collections team loads at 07:00 was never written.
Why it hid: the same code had run every night for months. With 200,000 contracts it used a lot of memory, but under the limit, so nothing alerted.
How we saw it: showed the exception, and the 's memory chart climbed in a straight line from 02:00 until the crash.
App Insights: the crash and the memory climb
exceptions
| where timestamp > ago(12h)
| where type has "OutOfMemory"
| project timestamp, outerMessage
In theory
An export should alert when it reads far more rows than it writes, or when its run time or memory jumps.
In practice
The only alert was on the crashing, so months of reading the whole table looked normal.
Try it yourself · 10 minutes, on your Mac
In a console app with EF Core and SQLite, add 1,000 contracts, 10 of them more than 30 days overdue.
Query with .AsEnumerable().Where(c => c.DaysOverdue > 30) and print the SQL with EF logging: no WHERE.
Move the before and print ToQueryString(): now it has WHERE.
Add a C# method to the and run it without : read the "could not be translated" error.
You should see
Where AsEnumerable sits decides where the filter runs. Look at the SQL, not only at the result.
The senior move
"AsEnumerable before a Where looks harmless and returns the right answer, but it loads the whole table. I keep filters translatable so SQL does them, I check ToQueryString in review, and I alert when a job reads far more rows than it writes."
SourcesSourced: EF Core's client-evaluation rules and the exact "could not be translated ... AsEnumerable" message (Microsoft Learn, client vs server evaluation); AsEnumerable before Where loads all rows (a run on .NET 10 with EF Core 9 and SQLite showed SELECT without WHERE, 3 of 5 rows kept in memory). Expression<Func<T,bool>> is translatable; a compiled method call is not.
Traps
Don't sayLINQ runs when you write the query.SayIt runs when you enumerate it: ,,,. Each enumeration runs it again.
Don't sayAdd when EF says it can't translate a method.SayThat moves every later step into memory and loads the whole table. Filter in SQL first, then switch to memory for the small remainder.
Don't say never throws.SayIt returns the default on no match, but throws if two or more rows match.
Don't saySkip and Take are enough for paging.SayWithout the order isn't guaranteed, so pages can repeat or skip rows.
Traps
What should you say instead? Tap to check.
Don't sayLINQ runs when you write the query.
Say instead
It runs when you enumerate it: ,,,. Each enumeration runs it again.
Don't sayAdd when EF says it can't translate a method.
Say instead
That moves every later step into memory and loads the whole table. Filter in SQL first, then switch to memory for the small remainder.
Don't say never throws.
Say instead
It returns the default on no match, but throws if two or more rows match.
Don't saySkip and Take are enough for paging.
Say instead
Without the order isn't guaranteed, so pages can repeat or skip rows.
Live coding
Easy first, then harder. Set a timer and type it before you look.
Task 1
Reportedeasy · 15 minutes
Write a LINQ query that lists every contract with its total paid and number of payments, including contracts with no payments (a left join: keep every contract, even with no match), highest total first.
gives each contract the list of its payments, empty if none.
What does return on an empty list?
Show a solution
Lending.sln · where this file sits
Program.cs · line 2 · 1 of 3A left join: every contract, each with the list of its payments (empty if none).
What the interviewer checks
Contracts with no payments appear, with 0.
(or query syntax with into and ), and can say which.
on the total.
Projects to a small object, not whole entities.
Senior extra: with EF Core, c.Payments.Sum(...) through the navigation property does the same in one query.
With AI tools
Copilot and EF Core's own error message both push the same wrong fix. When a query uses a C# method that EF can't turn into SQL, the error itself offers to "switch to client evaluation explicitly by inserting a call to 'AsEnumerable'" or , and assistants suggest exactly that. In review I check where any or sits: after the database filters is fine; before them it loads the whole table. I also check ToQueryString() output for anything that should have been a WHERE.