C# is the dominant language at Microsoft, enterprise .NET shops, Unity game studios, and fintech companies running on Windows infrastructure. These questions appear in senior backend interviews — interviewers expect you to explain not just what features do, but how they work internally.
1. How does async/await work internally?
The C# compiler transforms an async method into a state machine class at compile time. Each await point becomes a state: the method suspends (returns an incomplete Task), and a continuation callback is registered on the awaited Task. When the awaited operation completes, the continuation resumes execution from the saved state.
// What you write:
public async Task<string> FetchDataAsync(string url)
{
var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
// What the compiler generates (simplified):
class FetchDataAsync_StateMachine : IAsyncStateMachine
{
int state = -1;
TaskAwaiter<HttpResponseMessage> awaiter1;
TaskAwaiter<string> awaiter2;
void MoveNext()
{
switch (state)
{
case -1: // initial
awaiter1 = httpClient.GetAsync(url).GetAwaiter();
if (!awaiter1.IsCompleted) { state = 0; return; }
goto case 0;
case 0:
var response = awaiter1.GetResult();
awaiter2 = response.Content.ReadAsStringAsync().GetAwaiter();
if (!awaiter2.IsCompleted) { state = 1; return; }
goto case 1;
case 1:
SetResult(awaiter2.GetResult());
}
}
}
Key implications: async methods do NOT block threads. When awaiting I/O, the thread returns to the thread pool and handles other work. This is why async scales — 1000 concurrent HTTP requests use ~10 threads, not 1000.
2. ConfigureAwait(false) — when and why?
By default, after an await completes, execution resumes on the original synchronization context (e.g., the UI thread in WPF/WinForms, or the ASP.NET request context). ConfigureAwait(false) tells the runtime: do not capture the synchronization context — resume on any available thread pool thread. Use ConfigureAwait(false) in library code and non-UI code to avoid deadlocks and improve performance. Deadlock scenario: UI thread calls .Result on an async method that tries to resume on the UI thread, which is blocked waiting for .Result — neither can proceed.
3. Value types vs reference types
Value types (struct, int, bool, DateTime, Span<T>) are stored inline — on the stack if local variables, or embedded in the containing object. Reference types (class, string, arrays) store a pointer on the stack; the object is on the heap. Implications: value types have no GC overhead (not tracked by GC), but boxing (storing a value type in an object variable) allocates a heap object — avoid boxing in hot paths. Structs should be small (<= 16 bytes) and immutable to avoid defensive copies.
4. What is LINQ deferred execution?
Most LINQ operators (Where, Select, OrderBy, GroupBy) are lazy — they return an IEnumerable that represents the query, not the result. The query executes when the enumerable is consumed (foreach, ToList(), First(), Count()). This enables query composition without intermediate allocations and is the foundation of expression tree-based ORMs (Entity Framework translates LINQ expressions to SQL at execution time, not at query definition time).
var query = numbers.Where(x => x > 10) // no execution yet
.Select(x => x * 2); // still lazy
var list = query.ToList(); // executes now — ONE pass through numbers
Immediate execution operators: ToList(), ToArray(), Count(), First(), Sum() — these force evaluation immediately.
5. How does the .NET garbage collector work?
The .NET GC is a generational, mark-and-sweep collector. Three generations:
- Gen 0: newly allocated objects. GC runs Gen 0 frequently (milliseconds). Most objects die young — ~95% collected here.
- Gen 1: objects that survived Gen 0. Buffer between short-lived and long-lived objects.
- Gen 2: long-lived objects (static data, caches, large objects). Full GC (Gen 0+1+2) is expensive — can take 100ms+ for large heaps. The Large Object Heap (LOH) stores objects >= 85KB and is only collected during Gen 2 GC.
Tuning: minimize Gen 2 GC by not holding long-lived references to short-lived objects. Use object pools (ArrayPool<T>, ObjectPool<T>) for frequently allocated/freed objects. Use Span<T> and Memory<T> to operate on buffers without allocation.
6. Span<T> and Memory<T> — why are they important?
Span<T> is a stack-only ref struct representing a contiguous region of memory (array, stack memory, or unmanaged memory). Operations on Span<T> are bounds-checked but zero-allocation — no copies, no GC pressure. Memory<T> is the heap-safe equivalent usable in async contexts (Span<T> cannot cross await points).
// Parse CSV without allocating substrings
ReadOnlySpan<char> line = "100,200,300".AsSpan();
while (true)
{
int comma = line.IndexOf(
Frequently Asked Questions
How does async/await work under the hood in C#?
The C# compiler transforms async methods into state machine classes at compile time. Each await point becomes a state number. When execution reaches an await, the method captures its local variables into the state machine object and registers a continuation callback on the awaited Task — then returns control to the caller (yielding the current thread). When the awaited Task completes (e.g., an HTTP response arrives on an I/O thread), the continuation is invoked: the state machine resumes from the saved state on a thread pool thread. This means async methods never block threads — a thread is only consumed while CPU work is actually happening, not while waiting for I/O. This is why ASP.NET Core can handle thousands of concurrent requests with tens of threads instead of one thread per request.
What is the difference between value types and reference types in C#?
Value types (int, bool, struct, DateTime, Span<T>) store their data directly. When assigned or passed to a method, the entire value is copied. Value types on the stack have no GC overhead — they are not individually tracked by the garbage collector. Reference types (class, string, arrays, delegates) store a reference (pointer) to a heap-allocated object. Assignment copies the reference, not the object — two variables can point to the same object. Objects on the heap ARE tracked by the GC. Performance implications: prefer value types for small, short-lived, immutable data to reduce GC pressure. Avoid boxing (storing a value type in an object variable or interface) — it allocates a heap wrapper, creating GC pressure. Use generic collections (List<int>) instead of ArrayList to avoid boxing. Structs become problematic when large (copying overhead) or mutable (defensive copies can cause bugs).
What is LINQ deferred execution and when does it matter?
LINQ query operators like Where, Select, OrderBy, and GroupBy are lazy — they return an IEnumerable that represents the query definition, not the result. The query does not execute until the enumerable is consumed by a foreach loop, ToList(), First(), Count(), or any other operator that forces evaluation. Deferred execution enables query composition: you can chain multiple operators and they execute in a single pass. It is also the foundation of Entity Framework LINQ-to-SQL translation — EF captures the LINQ expression tree and translates the entire chain to SQL only when consumed, allowing the database to apply filtering before returning data to the application. Common mistake: iterating a deferred query multiple times re-executes the query each time. Call ToList() to materialize the results once and reuse them. Another trap: capturing mutable variables in a LINQ closure — the closure captures the variable reference, not the value at query definition time.
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "How does async/await work under the hood in C#?",
"acceptedAnswer": {
"@type": "Answer",
"text": "The C# compiler transforms async methods into state machine classes at compile time. Each await point becomes a state number. When execution reaches an await, the method captures its local variables into the state machine object and registers a continuation callback on the awaited Task — then returns control to the caller (yielding the current thread). When the awaited Task completes (e.g., an HTTP response arrives on an I/O thread), the continuation is invoked: the state machine resumes from the saved state on a thread pool thread. This means async methods never block threads — a thread is only consumed while CPU work is actually happening, not while waiting for I/O. This is why ASP.NET Core can handle thousands of concurrent requests with tens of threads instead of one thread per request."
}
},
{
"@type": "Question",
"name": "What is the difference between value types and reference types in C#?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Value types (int, bool, struct, DateTime, Span) store their data directly. When assigned or passed to a method, the entire value is copied. Value types on the stack have no GC overhead — they are not individually tracked by the garbage collector. Reference types (class, string, arrays, delegates) store a reference (pointer) to a heap-allocated object. Assignment copies the reference, not the object — two variables can point to the same object. Objects on the heap ARE tracked by the GC. Performance implications: prefer value types for small, short-lived, immutable data to reduce GC pressure. Avoid boxing (storing a value type in an object variable or interface) — it allocates a heap wrapper, creating GC pressure. Use generic collections (List) instead of ArrayList to avoid boxing. Structs become problematic when large (copying overhead) or mutable (defensive copies can cause bugs)."
}
},
{
"@type": "Question",
"name": "What is LINQ deferred execution and when does it matter?",
"acceptedAnswer": {
"@type": "Answer",
"text": "LINQ query operators like Where, Select, OrderBy, and GroupBy are lazy — they return an IEnumerable that represents the query definition, not the result. The query does not execute until the enumerable is consumed by a foreach loop, ToList(), First(), Count(), or any other operator that forces evaluation. Deferred execution enables query composition: you can chain multiple operators and they execute in a single pass. It is also the foundation of Entity Framework LINQ-to-SQL translation — EF captures the LINQ expression tree and translates the entire chain to SQL only when consumed, allowing the database to apply filtering before returning data to the application. Common mistake: iterating a deferred query multiple times re-executes the query each time. Call ToList() to materialize the results once and reuse them. Another trap: capturing mutable variables in a LINQ closure — the closure captures the variable reference, not the value at query definition time."
}
}
]
}