---
title: C# Interview Questions & Answers (2026): OOP, .NET & Async
description: C# interview questions that actually get asked in 2026 — OOP, value vs reference types, LINQ, async/await, garbage collection and the CLR — with real answers.
url: https://usegreenroom.app/blog/csharp-interview-questions
last_updated: 2026-06-20
---

← Back to blog

Technical

# C# interview questions and answers

June 20, 2026 · 35 min read

![C# interview questions and answers — cover from Greenroom, the AI mock interviewer](/assets/blog/csharp-interview-questions-hero.webp)

You're in a live coding round, screen-shared, twenty-five minutes in. The interviewer asks you to fetch a user's profile and return their display name from a method that calls an API. You write an `async` method like you've done a hundred times — except this time, under pressure, your fingers do something they haven't done since a 2019 Stack Overflow answer you half-remember: you call `GetUserProfileAsync(id).Result` instead of awaiting it, because typing `.Result` *feels* like finishing the sentence, and `await` feels like more characters than you have patience for at minute twenty-five.

You hit run. Nothing happens. Not an error — nothing. The cursor blinks. The interviewer's webcam light is still on. Fifteen seconds pass, which in an interview is roughly the length of a geological era. You say "it's just loading," with the practiced calm of someone who is lying to both the interviewer and themselves. Thirty seconds. You're now mentally rehearsing how to explain a hung process without using the word "hung." The interviewer, who has watched this exact deadlock happen at least forty times in their career, finally asks, gently: "What thread is `.Result` blocking, and what is that task's continuation waiting for?" You open your mouth. What comes out is not an answer. It's the sound of a synchronization context closing in on itself, forever, in real time, on a Zoom call, with your camera on.

This is, almost beat for beat, what a **C# interview** does to candidates who've memorized syntax without ever being asked to explain *why* the runtime behaves the way it does. C# interviews are not really testing whether you can write a `for` loop — any language can do that — they're testing whether you understand the machinery underneath: how value types and reference types actually live in memory, how the CLR turns your source into running code, how `async`/`await` rewrites your method into a state machine instead of literally pausing a thread, and why a single misplaced `.Result` can freeze an entire request pipeline. This guide covers the **C# interview questions** that actually come up in 2026 — OOP fundamentals with real C# code, value vs reference semantics, the CLR and garbage collector, delegates and events, LINQ's deferred execution model, the async/await machinery, and the specific gotcha questions ("what does this print") that separate candidates who've used C# from candidates who understand it — organized by area, with a real answer and a note on what each one is actually testing.

## How most people actually prepare for a C# interview — and where it falls short

Before the question bank, it's worth being honest about how people prepare for this specific interview, because the gap between "knows the material" and "passes the interview" is wider for C# than for most languages — precisely because so much of what's tested is *runtime behavior*, not syntax you can eyeball from a code sample.

**GeeksforGeeks-style "C# interview questions" dumps.** These are genuinely useful for knowing what topics show up — you'll recognize a good chunk of this guide's structure if you've browsed one. The failure mode is treating the dump as an answer key rather than a topic list. A GfG-style page gives you "explain boxing and unboxing" and a clean three-sentence answer; it does not give you the follow-up an actual interviewer asks next, which is usually "okay, now tell me why putting `int`s into an `ArrayList` is slower than a `List<int>`, specifically" — a question that requires you to connect boxing to a concrete collection-type decision, not recite a definition.

**LeetCode grinding with zero verbal practice.** LeetCode is excellent for algorithmic fluency and even decent for C# syntax reps, but a C# interview round is rarely *just* a DSA problem in a C# wrapper — it's frequently a conversation about why `IQueryable` chaining behaves differently from `IEnumerable`, or why your `async` method deadlocked, with no single "correct" leetcode-style output to check against. You can solve 300 problems in C# and still never once have explained, out loud, why a generational garbage collector exists.

**Microsoft Learn and the official .NET docs, read passively.** The official documentation is accurate and thorough — genuinely the best source for facts, and we cite it as ground truth throughout this guide. The gap is that reading "Task represents an asynchronous operation" is not the same skill as being asked, mid-interview, "so if Task isn't a thread, why does `await someHttpCall` free up the thread pool?" and having to construct that explanation live, from a slightly nervous, slightly tired brain, with someone waiting on the other end of the call.

**A friend's WhatsApp-forwarded "C# Interview Questions PDF.zip."** Every campus hiring cycle in India produces at least one of these, photocopied in spirit a hundred times since 2014, with answers that were already slightly outdated when first written (several still describe `Task.Result` as a normal pattern, which should tell you something). They're a fine way to see what *used* to be asked. They are not a safe way to learn what's asked *now* — default interface methods (C# 8), `using` declarations, and nullable reference types don't appear in PDFs that predate them.

**Generic ChatGPT-generated answer lists.** Asking an LLM "give me C# interview questions and answers" produces something that reads correctly and is genuinely useful for a first pass — but typing a prompt and reading the response back is a different skill from being asked a question out loud and having to produce the answer under a real follow-up, live, with no chance to silently re-read or regenerate. It also tends to flatten nuance: a generic list will tell you `ref` and `out` "both pass by reference," and stop, without making you explain the actual interview-relevant distinction (initialization requirements) that a sharp interviewer will probe next.

The honest throughline: every one of these methods is fine for *recall*. None of them rehearses the specific skill C# interviews are actually grading, which is explaining runtime *behavior* out loud, under a follow-up you didn't anticipate, the way the deadlock scene at the top of this post plays out. That's the gap [Greenroom](/)'s spoken mock-interview format is built to close — you talk through a C# concept out loud, the AI interviewer asks a real follow-up the way a human one would (not "what is async/await," but "why does this specific code deadlock, and why wouldn't the same code deadlock in an ASP.NET Core minimal API"), and you get feedback on the clarity of your explanation, not just whether you used the right vocabulary. It's not a replacement for actually learning the CLR's behavior — you still need the reps below — but it's the only one of these methods that trains the verbal, interrupted, defend-your-answer format the real interview actually is.

## OOP fundamentals

### Explain the four pillars of OOP with C# examples.

**Encapsulation** means bundling data and the methods that operate on it together, and hiding internal state behind a public interface — a `BankAccount` class exposing `Deposit()`/`Withdraw()` methods while keeping its `balance` field `private`. **Abstraction** means exposing only what a consumer needs and hiding implementation detail, typically via an abstract class or interface — callers depend on `IPaymentGateway`, not on whether it's Stripe or Razorpay underneath. **Inheritance** lets a derived class reuse and extend a base class's members (`class SavingsAccount : BankAccount`), but C# interviewers care more about whether you know *when not to* use it — favoring composition when the relationship isn't truly "is-a." **Polymorphism** lets the same method call behave differently depending on the runtime type, via method overriding (`virtual`/`override`) or interface implementation — the classic example is a `List<Shape>` where calling `.Area()` on each element runs a different calculation depending on whether it's a `Circle` or `Rectangle`.

```csharp
abstract class Shape
{
    public abstract double Area();
}

class Circle : Shape
{
    public double Radius;
    public override double Area() => Math.PI * Radius * Radius;
}

class Rectangle : Shape
{
    public double Width, Height;
    public override double Area() => Width * Height;
}

List<Shape> shapes = new() { new Circle { Radius = 2 }, new Rectangle { Width = 3, Height = 4 } };
foreach (var s in shapes)
    Console.WriteLine(s.Area()); // 12.566... then 12 — same call, different behavior
```

For a broader walkthrough of how interviewers probe these four pillars across languages, not just C#, our [OOP interview guide](/blog/oops-interview-questions) covers the cross-language version of this same question.

### Abstract class vs interface — what changed with default interface methods?

An abstract class can hold field state, constructors, and a mix of implemented and abstract members, but a class can inherit from only one. An interface traditionally held only member signatures with no implementation, but a class can implement any number of them. Since C# 8, interfaces can also define **default interface methods** — a method body right on the interface — which narrows the gap considerably and is mainly used so library authors can add new members to a public interface without breaking every existing implementation.

```csharp
interface ILogger
{
    void Log(string message);

    // default interface method — added later without breaking existing implementers
    void LogError(string message) => Log($"ERROR: {message}");
}

class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);
    // LogError() is inherited from the interface's default body — no need to implement it
}
```

The practical rule interviewers want to hear: reach for an abstract class when you need shared state or a common base implementation, and an interface when you're defining a contract that unrelated types should be able to satisfy. A common follow-up: "if interfaces can have bodies now, why do abstract classes still exist?" — the answer is that interfaces still can't hold instance fields, so any shared *state* (not just shared behavior) still forces you toward an abstract class.

### Sealed classes — what do they buy you, and why would you use one?

A `sealed` class cannot be inherited from further — `sealed class Logger { }` means no one can write `class FileLogger : Logger`. Two reasons interviewers expect: design intent (you're explicitly stopping extension because the class wasn't designed to be safely subclassed — overriding a method that's called from a constructor, for instance, is a classic source of bugs in derived classes) and performance (the JIT can sometimes devirtualize calls on a sealed type, since it knows no override can exist at a different level of the hierarchy, which occasionally matters in hot paths). `string` itself is sealed in .NET — you cannot subclass `string`, which is part of why it's safe for the runtime to make strong guarantees about its immutability and interning behavior.

### What's the difference between `==`, `.Equals()`, and `ReferenceEquals()` in C#?

For reference types, `==` by default compares references (are these the same object in memory), and `.Equals()` does the same unless the type overrides it. `string` is a special case: it overrides both `==` and `.Equals()` to compare content rather than reference, which is why `"abc" == "abc"` is `true` even though they could be different string instances. `ReferenceEquals()` is the one method that *cannot* be overridden — it always checks raw reference identity, which makes it the tool you reach for specifically when you need to bypass any custom equality logic and ask "are these literally the same object."

```csharp
string a = "hello";
string b = "hello";
string c = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });

Console.WriteLine(a == b);                    // True  — value comparison, string overrides ==
Console.WriteLine(a.Equals(b));                // True  — same reason
Console.WriteLine(ReferenceEquals(a, b));       // True  — string interning, see below
Console.WriteLine(ReferenceEquals(a, c));       // False — c was built at runtime, not interned
```

For custom classes, you override `Equals()` (and `GetHashCode()` alongside it — never one without the other) when you want value-based equality, like two `Point` objects with the same `X`/`Y` being considered equal. Interviewers ask this specifically to check whether you understand the `GetHashCode`/`Equals` contract, since breaking it silently corrupts dictionaries and hash sets — if two objects are `Equals()` but return different hash codes, a `Dictionary` will fail to find a key that's logically present, and the bug only shows up under specific hash collisions, which makes it miserable to debug in production.

```csharp
class Point
{
    public int X, Y;
    public override bool Equals(object? obj) =>
        obj is Point p && p.X == X && p.Y == Y;
    public override int GetHashCode() => HashCode.Combine(X, Y);
}
```

### `ref` vs `out` parameters — when do you use each?

Both pass a parameter by reference instead of by value, so the called method can modify the caller's variable directly. `ref` requires the variable to be initialized before the call, and the method can read it before optionally overwriting it. `out` doesn't require prior initialization, but the method *must* assign it before returning — the classic use case is `bool TryParse(string s, out int result)`, where the return value signals success and `out` delivers the parsed value without throwing on bad input.

```csharp
public bool TryParseAge(string input, out int age)
{
    if (int.TryParse(input, out age) && age >= 0)
        return true;
    age = 0;
    return false;
}
```

The interview signal: knowing `Try*` methods exist specifically to avoid exception-driven control flow on the hot path of parsing untrusted input — throwing and catching a `FormatException` for every malformed string in a high-volume input pipeline is measurably slower than a `TryParse` check, because exceptions carry real CLR overhead (stack unwinding, stack trace capture) that a boolean return doesn't.

## Value types, reference types & the CLR

### Value types vs reference types — struct vs class.

Value types (`int`, `bool`, `struct`, `enum`) store their data directly, are typically stack-allocated when local, and copying the variable copies the entire value — two independent copies that don't affect each other. Reference types (`class`, `string`, arrays, delegates) store a reference to data on the managed heap, so copying the variable copies the reference, and both variables point to the same underlying object — mutating through one is visible through the other. A quick comparison interviewers like to see written out:

```csharp
struct PointStruct { public int X, Y; }
class PointClass { public int X, Y; }

var a = new PointStruct { X = 1 };
var b = a;        // copies the value
b.X = 99;         // a.X is still 1

var c = new PointClass { X = 1 };
var d = c;        // copies the reference
d.X = 99;         // c.X is now 99 too — same object
```

Use a `struct` for small, short-lived, immutable-feeling data (a `Point`, `Money`, `DateRange`) where copy semantics are actually what you want and the type is small enough that copying is cheap; default to `class` for everything else, especially anything with identity or that's expensive to copy. A sharp follow-up interviewers ask: "is a local value-type variable *always* on the stack?" — the honest answer is no. A value type captured by a closure, boxed into an `object`, or stored as a field of a class instance lives wherever its container lives — on the heap, if the container is a reference type. "Value types live on the stack" is the simplified, slightly-wrong version of the rule; "value types are stored inline wherever their containing scope lives" is the correct one.

### What is boxing and unboxing, and why does it cost performance?

Boxing wraps a value type in an object so it can be treated as a reference type — assigning an `int` to an `object` variable allocates a new object on the heap and copies the int's value into it. Unboxing reverses this: casting that `object` back to `int` checks the runtime type and copies the value back out. Both directions cost a heap allocation (boxing) or a type check plus copy (unboxing), which matters in hot loops.

```csharp
int i = 42;
object boxed = i;          // boxing — heap allocation happens here
int unboxed = (int)boxed;  // unboxing — type-checked copy back out

var list = new ArrayList(); // non-generic — pre-generics era
list.Add(42);                // boxes every int added

var typed = new List<int>(); // generic — CLR generates a specialized version for int
typed.Add(42);                // no boxing at all
```

The classic example is putting `int`s into a non-generic `ArrayList`, which boxes every single one, versus a generic `List<int>`, which doesn't box at all because the CLR generates a specialized version of the collection for `int`. Interviewers ask this to see if you instinctively reach for generics rather than non-generic collections from the 1.0 era — and a stronger follow-up is asking you to spot boxing in less obvious places, like passing an `int` to a method expecting `params object[]`, or storing a `struct` in a non-generic interface-typed variable.

### What's the deal with nullable value types — `int?`, `Nullable<T>`?

Value types can't normally be `null` — an `int` always has *some* value, even if it's `0` by default. `Nullable<T>` (the `T?` shorthand) wraps a value type with an extra boolean flag so it can represent "no value," which matters constantly when modeling database columns or optional form fields that map to value types. `int? age = null;` compiles because `int?` is really `Nullable<int>` under the hood — a struct with a `HasValue` bool and a `Value` field — not the same as a reference type being null. Interviewers sometimes pair this with nullable *reference* types (the `string?` annotations introduced in C# 8 with nullable reference type analysis enabled) — the key distinction to articulate: `Nullable<T>` is a real runtime wrapper type for value types, while nullable reference type annotations are purely a compile-time analysis feature with no runtime representation at all — `string?` and `string` compile to the exact same IL.

```csharp
int? maybeAge = null;
if (maybeAge.HasValue)
    Console.WriteLine(maybeAge.Value);
else
    Console.WriteLine("no age provided");

int actual = maybeAge ?? 0; // null-coalescing default
```

### What is the CLR, and what happens between writing C# and running it?

The C# compiler (`csc` or Roslyn) compiles your source into **Intermediate Language (IL)**, a CPU-independent bytecode, packaged into an assembly (a `.dll` or `.exe`) along with metadata. At runtime, the **Common Language Runtime (CLR)** loads the assembly, and the **JIT (Just-In-Time) compiler** translates IL into native machine code method-by-method, the first time each method actually runs — not all at once at startup. This is also how C# interoperates with F# or VB.NET in the same solution: they all compile down to the same IL, so the CLR doesn't care which .NET language produced it. Interviewers use this to check whether you understand "compiled" languages aren't all compiled the same way — C# is JIT-compiled per method at runtime, not ahead-of-time like C, though `ReadyToRun`/Native AOT compilation exists for startup-sensitive scenarios like serverless functions and CLI tools, where paying the JIT cost on every cold start is unacceptable.

### What are assemblies, and what's the difference between the GAC and NuGet packages?

An assembly is the unit of deployment and versioning in .NET — a `.dll` or `.exe` containing IL code, type metadata, and a manifest describing its dependencies and version. Historically, shared assemblies could be installed machine-wide in the **Global Assembly Cache (GAC)** so multiple applications could reference one shared copy; modern .NET (Core and later) mostly abandoned the GAC in favor of **NuGet packages** restored per-project into a local cache, which avoids the classic "DLL hell" of one app needing version 1 of a shared assembly while another needs version 2. The practical interview point: modern .NET strongly prefers self-contained, per-project dependency resolution over shared global state.

## Delegates, events & LINQ

### What is a delegate? How do `Func`, `Action`, and `Predicate` differ?

A delegate is a type-safe reference to a method — it lets you pass a method around like a variable, store it, and invoke it later, which is the foundation of callbacks and event handling in C#. `Func<T, TResult>` represents a method that takes parameters and returns a value (`Func<int, int, int> add = (a, b) => a + b`), `Action<T>` represents a method that takes parameters and returns nothing (`Action<string> log = msg => Console.WriteLine(msg)`), and `Predicate<T>` is specifically a method that takes one argument and returns `bool`, commonly used in filtering (`list.FindAll(x => x > 5)` is conceptually a `Predicate<int>`). Interviewers want to hear that these three are really just pre-defined generic delegate types — you could declare your own custom delegate type, but these cover the vast majority of cases so nobody bothers anymore.

### Multicast delegates — what happens when a delegate has more than one subscriber?

A delegate in C# is multicast by default — `+=` adds another method to its invocation list rather than replacing the existing one, and invoking the delegate calls every subscriber in order. This matters for return values: if a multicast delegate's underlying type returns something other than `void`, invoking it only gives you back the *last* subscriber's return value — the others run, but their results are silently discarded unless you manually walk `GetInvocationList()`.

```csharp
Action<string> notify = msg => Console.WriteLine($"Email: {msg}");
notify += msg => Console.WriteLine($"SMS: {msg}");
notify += msg => Console.WriteLine($"Push: {msg}");

notify("Order shipped");
// Email: Order shipped
// SMS: Order shipped
// Push: Order shipped     -- all three ran, in subscription order
```

This is exactly why `event` exists as a distinct, restricted concept on top of multicast delegates rather than every event just being a public field — see below.

### Explain events and the observer pattern in C#.

An event is a delegate-based publish/subscribe mechanism: a class exposes an `event` field (typically of type `EventHandler` or a custom delegate type), and any number of other objects can `+=` a handler method onto it without the publisher knowing or caring who's listening. When something happens, the publisher calls the event (often via a protected `OnSomethingHappened()` method that null-checks first), and every subscribed handler runs.

```csharp
class OrderProcessor
{
    public event Action<string>? OrderPlaced;

    public void PlaceOrder(string orderId)
    {
        // ... processing logic ...
        OrderPlaced?.Invoke(orderId); // null-conditional — fires only if someone subscribed
    }
}

var processor = new OrderProcessor();
processor.OrderPlaced += id => Console.WriteLine($"Sending confirmation for {id}");
processor.OrderPlaced += id => Console.WriteLine($"Logging order {id}");
processor.PlaceOrder("ORD-1042");
```

This is C#'s built-in implementation of the observer pattern — a button's `Click` event, a `PropertyChanged` notification in MVVM, or a custom `OrderPlaced` event in a domain model are all the same shape. The detail interviewers probe: events restrict what subscribers can do compared to a raw delegate field — you can't invoke someone else's event from *outside* the declaring class, and you can't overwrite other subscribers with `=` instead of `+=` — which is exactly why events exist as a distinct language feature rather than just public delegate fields. A public `Action` field would let any external code call `processor.OrderPlaced = null`, silently wiping out every other subscriber; a real `event` doesn't allow that from outside the class.

### Lambda expressions and LINQ — explain deferred vs immediate execution.

A lambda expression (`x => x.Age > 18`) is shorthand syntax for an anonymous method, most often used as the predicate or projection passed into a LINQ operator. LINQ operators split into two execution models: most (`Where`, `Select`, `OrderBy`) are **deferred** — they build up a query description but don't actually run it until you enumerate the result (`foreach`, `.ToList()`, `.Count()`), which means the underlying data source is read at enumeration time, not at the line where you wrote the query. A few operators (`ToList()`, `ToArray()`, `Count()`, `First()`) force **immediate** execution right there. This distinction causes a real, frequently-tested bug: building a query against a mutable list, mutating the list, then enumerating the query later and getting results that reflect the *mutated* state, because the query never actually ran until that final enumeration.

```csharp
var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n > 1);   // not executed yet
numbers.Add(4);
foreach (var n in query) Console.Write(n); // prints 2 3 4 — Add() was seen
```

### A worked LINQ chain — what does it actually do, step by step?

A typical interview ask is to read or write a multi-step LINQ chain and narrate what each operator contributes, since that's a good proxy for whether you actually understand the pipeline rather than pattern-matching syntax.

```csharp
var topSpenders = orders
    .Where(o => o.Status == "Completed")              // filter — deferred
    .GroupBy(o => o.CustomerId)                         // group by key — deferred
    .Select(g => new { CustomerId = g.Key, Total = g.Sum(o => o.Amount) }) // project — deferred
    .OrderByDescending(x => x.Total)                    // sort — deferred
    .Take(5)                                             // limit — deferred
    .ToList();                                           // immediate execution happens HERE
```

Nothing touches the underlying `orders` collection until `.ToList()` runs — every prior line just builds up a description of the work. The interview-relevant point to say out loud: because the whole chain is deferred until the final materialization, the LINQ provider (in-memory `IEnumerable` here, or a database provider if `orders` were an EF Core `DbSet`) gets to see the *entire* pipeline before deciding how to execute it — which is exactly what makes the next question matter so much.

### IEnumerable vs IQueryable — why does this matter for database calls?

`IEnumerable<T>` represents an in-memory sequence — when you call `.Where()` on it, you get a compiled delegate that runs against objects already loaded into memory. `IQueryable<T>` represents a query that hasn't been executed yet against an external data source (usually a database via EF Core) — calling `.Where()` on it builds up an **expression tree**, not a compiled delegate, which the provider translates into SQL only when you finally enumerate it.

```csharp
// IQueryable — filtering happens in SQL, only matching rows cross the wire
var activeUsers = dbContext.Users
    .Where(u => u.IsActive)
    .Select(u => u.Email)
    .ToList();   // SQL: SELECT Email FROM Users WHERE IsActive = 1

// the N+1 trap — looks fine, silently issues one query per user
var names = dbContext.Users.ToList();          // pulls every user into memory NOW
foreach (var u in names)
    Console.WriteLine(u.Orders.Count);          // each .Orders access can trigger its own query
```

The interview trap: chaining `.Where()` calls on an `IQueryable` keeps building SQL (filtering happens in the database, only matching rows come over the wire), but calling `.AsEnumerable()` or `.ToList()` too early switches you to `IEnumerable`, silently pulling the *entire* table into memory before any further `.Where()` runs client-side. The closely related **N+1 query problem** shown above happens when lazy-loaded navigation properties (`u.Orders`) trigger a separate database round-trip per row in a loop instead of one batched query — the fix is `.Include(u => u.Orders)` to eager-load the relationship up front, or projecting exactly the shape you need inside the original `IQueryable` so EF Core can fold it into a single SQL statement. This single distinction is responsible for a large share of real-world EF Core performance bugs, and it's one of the most reliable "do they actually understand the ORM" filters interviewers have.

For more on how interviewers structure data-and-query-shaped questions generally — not LINQ-specific — see our [data structures interview questions](/blog/data-structures-interview-questions) guide and our [SQL interview questions](/blog/sql-interview-questions) guide, since the underlying "where does this actually execute" reasoning is the same skill in both.

![C# interview topics — OOP, value vs reference types, delegates, LINQ, async](/assets/blog/pool-structured-screen.webp)

C# rounds test OOP depth plus .NET specifics like delegates, LINQ and async.

## Async & concurrency

### How does async/await actually work in C#?

`async` marks a method that can contain `await` expressions, and `await` suspends execution of *that method* until the awaited `Task` completes — without blocking the calling thread, which goes back to the thread pool to do other work in the meantime. Under the hood, the compiler rewrites the method into a **state machine**: each `await` becomes a point where the method can pause and later resume, with local variables captured as fields on a compiler-generated struct or class implementing `IAsyncStateMachine`. This is precisely why async/await is described as "syntactic sugar over continuations" — you write code that reads top-to-bottom, but the compiler turns it into callback-driven continuation logic, the same shape you'd otherwise hand-write with `.ContinueWith()`.

```csharp
public async Task<string> GetUserNameAsync(int id)
{
    var response = await httpClient.GetAsync($"/users/{id}");
    var json = await response.Content.ReadAsStringAsync();
    return ParseName(json);
}
```

Roughly, the compiler turns that into something conceptually like a switch-case state machine that tracks "which `await` am I currently resuming from," storing `response` and `json` as fields rather than local stack variables, because the method's execution can be suspended and resumed on a *different* thread-pool thread than the one it started on — which is also why you can't safely assume "the code after my `await` runs on the same thread as the code before it" unless you've explicitly preserved a synchronization context.

### Task vs Thread — what's the real difference?

A `Thread` is an actual OS-level thread — expensive to create (roughly a megabyte of stack by default) and a scarce resource. A `Task` represents an asynchronous *operation*, which may or may not run on a dedicated thread at all — `Task.Run()` schedules work onto the thread pool, but `await someHttpCall` doesn't occupy any thread while waiting on I/O, because there's nothing for a thread to do until the network response arrives. This is the core reason async I/O scales so much better than spinning up a thread per request: a server handling 10,000 concurrent slow I/O calls with `async`/`await` needs only a handful of pool threads, while doing the same with one blocking thread per call would exhaust the OS. Interviewers ask this to check you know `Task` is a unit of *work*, not a unit of *thread*.

### What's the deadlock pitfall with `.Result`, `.Wait()`, and `ConfigureAwait` — the exact scenario from the intro?

This is the deadlock that opened this guide, and it's worth walking through precisely because "it can deadlock" is a memorized fact, while explaining *why* is the actual interview signal.

```csharp
// On a context with a captured synchronization context (classic ASP.NET / WPF / WinForms):
public string GetUserNameSync(int id)
{
    var task = GetUserNameAsync(id); // kicks off the async method
    return task.Result;               // BLOCKS this thread waiting for the task
}
```

Calling `.Result` or `.Wait()` on a `Task` from a context with a captured synchronization context (classically, ASP.NET (Framework) request contexts or UI thread contexts) can deadlock: the blocking call holds that context's thread waiting for the task to finish, but the task's continuation — the code *after* the `await` inside `GetUserNameAsync` — needs to resume on that *same* captured context, which is now blocked waiting for `.Result` to return. Thread A is waiting on Task B; Task B's continuation is waiting for Thread A to be free. Neither side moves. Forever — or until a request timeout kills it, which is the production version of the Zoom-call freeze from the cold open.

The fix in library code is `ConfigureAwait(false)`, which tells the awaited task's continuation it doesn't need to resume on the original context, so it can run on any available thread pool thread instead:

```csharp
public async Task<string> GetUserNameAsync(int id)
{
    var response = await httpClient.GetAsync($"/users/{id}").ConfigureAwait(false);
    var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
    return ParseName(json);
}
```

Modern ASP.NET Core doesn't install that synchronization context by default, which is why this exact deadlock is less commonly *reproduced* today than it was on ASP.NET (Framework) or in WPF apps — but it's still a near-guaranteed interview question, because "async all the way down, never block on async code with `.Result`/`.Wait()`" is the rule, and explaining the circular-wait mechanism (not just naming it) is what separates a strong answer from a memorized one.

### What does `Task.WhenAll` buy you over awaiting tasks sequentially?

Awaiting several independent `Task`s one at a time (`await Task1(); await Task2();`) runs them sequentially even though nothing requires that — each `await` blocks progress to the next line until that specific task finishes.

```csharp
// sequential — total time ≈ sum of all three calls
var user = await GetUserAsync(id);
var orders = await GetOrdersAsync(id);
var reviews = await GetReviewsAsync(id);

// concurrent — total time ≈ the slowest of the three
var userTask = GetUserAsync(id);
var ordersTask = GetOrdersAsync(id);
var reviewsTask = GetReviewsAsync(id);
await Task.WhenAll(userTask, ordersTask, reviewsTask);
var user2 = userTask.Result;       // safe here — task is already complete
var orders2 = ordersTask.Result;
var reviews2 = reviewsTask.Result;
```

Starting all the tasks first and then awaiting them together with `await Task.WhenAll(...)` lets them run concurrently, so total wall-clock time is roughly the slowest one rather than the sum of all of them. The same tradeoff interviewers test in JavaScript (`Promise.all`) applies identically here — sequential `await` in a loop is a common, easy-to-miss source of needless latency in C# services that call multiple independent downstream APIs. Note that calling `.Result` on `userTask` after `Task.WhenAll` has already completed is safe — it's not the same deadlock risk as blocking on an *incomplete* task, since the value is already available and there's no continuation left waiting to run.

## Garbage collection & memory

### How does .NET garbage collection work — what are generations?

The .NET GC manages heap memory automatically using a generational model based on the observation that most objects die young. **Generation 0** holds newly allocated objects and is collected most frequently and cheaply; objects that survive a Gen 0 collection are promoted to **Generation 1**, a buffer between short-lived and long-lived objects; objects that survive further are promoted to **Generation 2**, collected far less often because scanning it is expensive. Large objects (85,000+ bytes) go straight into the separate **Large Object Heap (LOH)**, which is collected as part of Gen 2 and isn't compacted by default, which is why holding many large, short-lived objects can cause LOH fragmentation. The interview signal: understanding *why* the generational split exists (cheap, frequent collection of short-lived garbage; rare, expensive collection of long-lived survivors) rather than just naming the generations.

A good follow-up to be ready for: "you're seeing GC pauses in production that don't show up locally — what's your first hypothesis?" A reasonable answer touches allocation rate (are you allocating far more per request than you think, e.g. via string concatenation in a loop or LINQ allocating intermediate collections), object lifetime (are short-lived objects accidentally being kept alive long enough to get promoted to Gen 2, where collection is expensive), and LOH fragmentation if large arrays or strings are involved — "but it works on my machine" GC pauses are very often a difference in load/allocation rate, not a difference in code.

### What is `IDisposable`, and why does the `using` pattern matter?

The GC reclaims *managed* memory, but it has no idea how to release *unmanaged* resources — open file handles, database connections, network sockets, OS-level graphics handles — because those live outside the managed heap entirely. `IDisposable.Dispose()` is the explicit, deterministic contract for releasing those resources the moment you're done with them, rather than waiting for an unpredictable GC pass (or never, if the object lives long enough to dodge collection for a while). The `using` statement (or `using` declaration in C# 8+) guarantees `Dispose()` runs even if an exception is thrown inside the block, which is the entire point — manual `try/finally` achieves the same thing, but `using` is how you signal "this resource has a deterministic lifetime" at a glance:

```csharp
// classic using statement — scoped to the block
using (var connection = new SqlConnection(connectionString))
{
    connection.Open();
    // connection.Dispose() runs here, even if an exception is thrown above
}

// using declaration (C# 8+) — scoped to the enclosing method/block, less nesting
using var connection = new SqlConnection(connectionString);
connection.Open();
// connection.Dispose() runs automatically at end of scope
```

### Finalizers vs Dispose — why do both exist, and how do they interact?

A finalizer (`~ClassName()`) is the closest C# equivalent to a destructor — the GC calls it, eventually, before reclaiming an object's memory, but you cannot control *when* that happens, which makes finalizers a poor primary mechanism for releasing scarce resources promptly. `Dispose()` is deterministic — you (or a `using` block) call it exactly when you're done. The recommended pattern combines both: implement `Dispose()` for the deterministic path, implement a finalizer as a safety net in case someone forgets to call `Dispose()`, and call `GC.SuppressFinalize(this)` inside `Dispose()` so the GC doesn't bother running the finalizer for an object that's already been cleaned up properly.

```csharp
class FileWrapper : IDisposable
{
    private IntPtr handle;
    private bool disposed = false;

    public void Dispose()
    {
        if (disposed) return;
        ReleaseHandle();
        disposed = true;
        GC.SuppressFinalize(this); // already cleaned up — skip the finalizer
    }

    ~FileWrapper()
    {
        ReleaseHandle(); // safety net if Dispose() was never called
    }

    private void ReleaseHandle() { /* release the unmanaged handle */ }
}
```

The interview-relevant nuance: a finalizer adds real cost (objects with a finalizer survive at least one extra GC generation longer than they otherwise would, because the GC has to run the finalizer on a separate thread before the memory can actually be reclaimed), so the right answer is never "always add a finalizer" — it's "add one only as a backstop for genuinely unmanaged resources, and make `Dispose()` the real story."

### Managed vs unmanaged code — what's the actual boundary?

Managed code runs under the CLR, which provides automatic memory management, type safety, and exception handling — essentially all ordinary C# code. Unmanaged code runs outside the CLR's control — native libraries, OS APIs, COM objects — and the CLR has no visibility into memory it allocates. C# crosses that boundary via **P/Invoke** (`[DllImport]`) to call native functions, and anything that wraps an unmanaged handle (a file, a socket, a native bitmap) needs to implement `IDisposable` and release that handle explicitly, because the GC can see and free managed memory but is completely blind to memory the unmanaged side allocated on your behalf.

## Gotcha questions: "what does this print?"

C# interviews love a short snippet and the question "what does this print, and why" — it's a fast, hard-to-fake way to check whether you actually understand a mechanism rather than its one-sentence summary. A few that come up constantly:

### Closures capturing a loop variable

```csharp
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (var a in actions) a();
```

In modern C# (5.0+), the loop variable in a `for` loop is scoped *per iteration*, so this correctly prints `0 1 2` — each closure captures its own `i`. This is a deliberate behavior change from C# 4 and earlier, where `for`-loop variables were scoped to the whole loop and every closure captured the *same* variable, printing `3 3 3` instead (since all three lambdas ran after the loop finished, when `i` was `3`). The version that still trips people up today is `foreach` with an *explicitly hoisted* variable outside the loop body, or capturing a variable declared before the loop:

```csharp
var actions = new List<Action>();
int shared = 0;
for (int i = 0; i < 3; i++)
{
    shared = i;
    actions.Add(() => Console.WriteLine(shared));
}
foreach (var a in actions) a(); // prints 2 2 2 — all closures share the same "shared" variable
```

The interview signal: knowing that closures capture *variables*, not *values* — what gets "frozen" depends on the variable's scope, not on when the lambda happens to be defined.

### String interning — why does `ReferenceEquals` sometimes surprise you?

```csharp
string a = "hello";
string b = "hello";
string c = new StringBuilder("hello").ToString();

Console.WriteLine(ReferenceEquals(a, b)); // True
Console.WriteLine(ReferenceEquals(a, c)); // False
```

String literals known at compile time are automatically **interned** — the CLR keeps a single shared instance per distinct literal value in a process-wide intern pool, so `a` and `b` above end up pointing at the exact same object. A string built at runtime (via `StringBuilder`, concatenation of variables, `Substring`, etc.) is a fresh heap allocation, not automatically interned, even if its content happens to match an existing literal — hence `ReferenceEquals(a, c)` being `False` despite both strings containing `"hello"`. You can manually intern a runtime string with `string.Intern()`, though it's rarely worth doing outside of genuinely memory-constrained, string-heavy workloads, since the intern pool itself isn't free and isn't garbage collected the same way ordinary heap objects are.

### Exception filters — what does the `when` clause actually do?

```csharp
try
{
    CallExternalApi();
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
    await Task.Delay(1000);
    RetryCall();
}
catch (HttpRequestException ex)
{
    LogAndRethrow(ex);
}
```

The `when` clause adds a boolean condition to a `catch` block — the exception is only caught by that block if both the type matches *and* the `when` condition is true; otherwise the runtime keeps looking at subsequent `catch` blocks (or propagates the exception further up if none match). The interview-relevant detail people miss: a `when` filter that evaluates to `false` does **not** unwind the stack and rethrow — the CLR evaluates filters *before* deciding to unwind at all, which means you can use a `when` clause purely for logging side effects (`catch (Exception ex) when (LogAndReturnFalse(ex))`) without actually catching anything, since a filter that always returns `false` lets every exception pass through untouched while still running your logging code on the way past.

### Boxing in disguise — the `params object[]` trap

```csharp
void LogValues(params object[] values)
{
    foreach (var v in values) Console.WriteLine(v);
}

int x = 5, y = 10;
LogValues(x, y); // both ints get boxed to satisfy "object[]"
```

This looks like ordinary method-calling syntax, but passing value types into a `params object[]` (or any `object`-typed parameter) boxes every one of them — a detail that's easy to miss because there's no visible cast anywhere in the call site. It's the same underlying cost as the `ArrayList` example earlier, just harder to spot, and it's exactly the kind of "spot the hidden allocation" question that separates candidates who've internalized boxing from candidates who can only define it.

<div class="verdict"><strong>The core truth:</strong> C# interviews reward knowing the .NET specifics, not just generic OOP — value vs reference semantics, why boxing costs an allocation, how async/await's state machine actually schedules work, deferred LINQ execution, and why the GC's generational model exists. Reciting "C# is object-oriented" gets you nowhere; explaining *why* `IQueryable` chaining matters for a real database query, or why a specific snippet deadlocks, is what gets offers.</div>

## Practise explaining, not just memorizing

You can recognize every answer above on a flashcard and still freeze when an interviewer asks you to explain why a particular LINQ query hit the database three times, live, with someone watching you reason through it — or why your `.Result` call just hung the entire request, the way it did in the cold open. The interview is verbal, so practice has to be verbal too — reading an explanation is a different skill from producing one under mild pressure with a follow-up you didn't script for. Strong C# candidates also tend to be strong communicators generally; if narrating your reasoning out loud is the part that feels unfamiliar rather than the C# content itself, our [coding interview communication tips](/blog/coding-interview-communication-tips) guide is worth pairing with this one.

[Greenroom](/) runs spoken C# and .NET mock interviews, asks follow-ups that probe whether you actually understand the behavior you just described, and gives feedback on how clearly you explain it — not just whether the final answer was technically correct. Pair it with our [OOP interview guide](/blog/oops-interview-questions), [backend developer interview questions](/blog/backend-developer-interview-questions), and — since C# backend roles increasingly come with a system-design round attached — [system design interviews: what they actually test](/blog/system-design-interviews-what-they-test).

For the canonical, vendor-neutral version of anything in this guide — the exact semantics of `ref struct`, the full generational GC algorithm, or the precise rules nullable reference types use for flow analysis — Microsoft Learn's C# and .NET documentation is the primary source we cross-checked this guide against, and it's worth bookmarking directly for anything this post simplifies for interview purposes.

## Frequently asked questions

### What are the most common C# interview questions?

The most common cover OOP fundamentals (the four pillars, abstract class vs interface, sealed classes), value vs reference types (struct vs class, boxing/unboxing, nullable value types), `==` vs `.Equals()` vs `ReferenceEquals()`, delegates (`Func`, `Action`, `Predicate`) and events, lambda expressions and LINQ (deferred vs immediate execution, `IEnumerable` vs `IQueryable`, the N+1 query problem), async/await and `Task` vs `Thread`, garbage collection with the `IDisposable`/`using` pattern, and gotcha snippets like closures capturing loop variables or string interning.

### What is the difference between value and reference types in C#?

Value types (like `int`, `struct`, and `enum`) store their data directly and are typically stack-allocated when local, so copying the variable copies the entire value as an independent copy. Reference types (like `class`, `string`, and arrays) store a reference to data on the heap, so copying the variable copies the reference, and both variables end up pointing at the same underlying object. This difference drives assignment semantics, equality comparisons, and how mutations through one variable become visible through another. A value type isn't *always* on the stack, though — one stored as a field of a class instance, or boxed into an `object`, lives on the heap with its container.

### How does async/await work in C#, and why does calling `.Result` sometimes deadlock?

`async` marks a method that can contain `await` expressions, and `await` suspends the method until an awaited `Task` completes, returning control to the caller without blocking the calling thread. The compiler builds a state machine that resumes the method from the right point once the awaited task finishes. Calling `.Result` or `.Wait()` synchronously blocks the calling thread, and on a context with a captured synchronization context (classic ASP.NET or UI threads), that block can deadlock — the blocked thread is waiting for the task, but the task's continuation needs that same thread to resume on. The fix in library code is `ConfigureAwait(false)`, and the broader rule is "async all the way down."

### Why does `IQueryable` matter more than `IEnumerable` for database-backed apps?

`IQueryable` builds an expression tree that a provider like EF Core translates into SQL only when you enumerate it, so filtering and projection happen inside the database and only the matching rows cross the wire. Switching to `IEnumerable` too early — via `.ToList()` or `.AsEnumerable()` before you're done filtering — pulls the entire result set into memory first, and every `.Where()` after that point runs in your application instead of the database. The closely related N+1 problem happens when lazy-loaded navigation properties trigger a separate query per row in a loop instead of one batched, eager-loaded query. This single distinction explains a large share of real-world EF Core performance bugs, which is why interviewers ask about it directly.

### What's the difference between Task and Thread in C#?

A `Thread` is an actual OS-level thread, expensive to create and a genuinely scarce resource. A `Task` represents an asynchronous unit of work that may run on a thread-pool thread, or may not occupy any thread at all while it's waiting on I/O — which is exactly why `async`/`await` over I/O scales to thousands of concurrent operations using only a handful of pool threads, where one dedicated thread per operation would exhaust the OS.

### What does the .NET garbage collector actually do, and why does it have generations?

The GC automatically reclaims managed heap memory using a generational model based on the observation that most objects die young. Generation 0 (newly allocated objects) is collected frequently and cheaply; survivors get promoted to Generation 1, then Generation 2, which is collected rarely because scanning it is expensive. Large objects (85,000+ bytes) go to a separate Large Object Heap collected alongside Gen 2 and not compacted by default. The GC handles managed memory only — unmanaged resources (file handles, sockets) still need explicit `IDisposable`/`using` cleanup, since the GC has no visibility into memory it didn't allocate.

### How should I prepare for a C# interview?

Focus on .NET specifics rather than generic OOP — value vs reference semantics, why boxing/unboxing costs an allocation, delegates and events, deferred LINQ execution, how the async/await state machine actually schedules work, and the generational garbage collector — since those are what separate strong candidates from people who can only define terms. Practise explaining the behavior out loud with a voice-based mock interview that asks realistic follow-ups, since real C# rounds rarely stop at your first, definition-level answer — they probe the "why," the way a deadlocked `.Result` call or an N+1 query bug forces you to reason through what actually happened underneath.

C# rounds reward knowing the .NET model, explained out loud under real follow-up questions. Greenroom runs spoken technical interviews that follow up on your reasoning. Free to start.
