Interview prep · Junior → Senior
C# Interview Questions
The questions a C# interviewer actually asks about the language —
the type system (the load-bearing value-vs-reference distinction), classes, structs and records,
inheritance and polymorphism, generics, delegates, events and lambdas, LINQ and iterators,
async/await, pattern matching, nullability and exceptions, and the classic
gotchas — each answered with a short example and a link to the source. This page covers the
C# language and its observable semantics; for the runtime and platform it runs on (the
CLR, garbage collection, the JIT) see the
.NET version reference.
Pair it with the
C# version reference
and the open C# roles on the jobs board.
Difficulty
Showing 0 of 0 questions
No questions match that combination. .
1 · Types & the type system
What is the difference between a value type and a reference type? Junior
The load-bearing C# distinction. A value type (a struct — including int, bool, DateTime, every enum) holds its data directly; assigning it or passing it to a method copies the value. A reference type (a class, plus string, arrays, delegates) holds a reference to data on the heap; assigning it copies the reference, so two variables point at the same object. The popular shorthand “value types live on the stack” is wrong — a struct that's a field of a class, or one that's been boxed, lives on the heap; where a value lives is an implementation detail, the copy-vs-share semantics are the contract.
struct PointV { public int X; } // value type class PointR { public int X; } // reference type var a = new PointV { X = 1 }; var b = a; b.X = 9; // a.X == 1 (copy) var c = new PointR { X = 1 }; var d = c; d.X = 9; // c.X == 9 (shared)
Why it's asked / follow-up: almost every other C# subtlety (struct mutation traps, boxing, == vs .Equals(), why a method can't change your int but can change your list) flows from this. Follow-up: “is string a value or reference type?” — a reference type, but immutable, so it behaves value-like and overloads == for value equality.
Source: C# reference — Value types / Reference types.
When should you use a struct instead of a class?
Mid
Reach for a struct only when the type is small, immutable, and logically a single value — a coordinate, a money amount, an RGBA color. Microsoft's own guidance: keep it under ~16 bytes, make it immutable (ideally a readonly struct), and don't box it often. Everything else should be a class. The reason is the copy semantics: structs avoid a heap allocation, but every assignment and argument pass copies all the bytes, and a mutable struct invites the “I changed a copy” bug (see the gotchas section). When in doubt, use a class.
readonly struct Money // good struct: small, immutable, one value { public Money(decimal amt) => Amount = amt; public decimal Amount { get; } } // A 12-field mutable "struct Order { ... }" is a class wearing the wrong hat.
Why it's asked / follow-up: it separates “knows the syntax” from “knows the cost.” Follow-up: “what does readonly struct buy you?” — the compiler enforces immutability and can skip defensive copies when the struct is passed by in reference. readonly struct arrived in C# 7.2.
Source: .NET design guidelines — Choosing between class and struct.
What does var do — is it dynamic typing?
Junior
No. var is compile-time type inference: the compiler reads the initializer and fixes the variable's static type once, exactly as if you'd typed it out. The variable is just as strongly typed as any other — var n = 5; makes n an int forever, and n = "x"; is a compile error. It is purely a syntactic convenience (it requires an initializer, and can't be a field, parameter, or return type). Contrast dynamic, which genuinely defers type resolution to runtime.
var n = 5; // inferred int — identical to: int n = 5; var items = new List<string>(); // avoids restating the type twice // n = "x"; // compile error: int can't hold a string
Why it's asked / follow-up: it weeds out the “var is like JavaScript” misconception. Follow-up: “when is var a bad idea?” — when the initializer doesn't make the type obvious to a reader (var x = GetThing();); style, not correctness.
int? vs string? — are they the same kind of “nullable”?
Mid
No — they're two different features that share a ?. int? is a nullable value type: real syntactic sugar for Nullable<int>, a struct with a HasValue flag and a Value. It changes the runtime representation. string? is a nullable reference type (NRT): a compile-time annotation only — at runtime it's still a plain string reference that could be null. string? tells the compiler “null is expected here, warn me if I don't check,” while plain string means “I promise this is non-null.”
int? a = null; // Nullable<int> — a struct; a.HasValue == false if (a is int v) { /* v is the unwrapped int */ } string? s = null; // just a string reference; ? is a compile-time hint // s.Length; // warning (CS8602): possible null dereference
Why it's asked / follow-up: the shared syntax hides a deep difference; mixing them up leads to wrong mental models about runtime cost. Follow-up: “does string? cost anything at runtime?” — nothing; the annotations are erased to attributes used only by the analyzer. NRTs arrived in C# 8.
Source: Nullable value types / Nullable reference types.
What is the difference between object and dynamic?
Mid
Both can hold any value, but the compiler treats them oppositely. object is the root of the type hierarchy and is fully statically checked — you can only call object's members until you cast. dynamic tells the compiler to stop checking: member access is resolved at runtime by the DLR, so any call compiles and a wrong one throws RuntimeBinderException at execution. dynamic is for genuine late-binding scenarios (COM interop, JSON-ish bags, IronPython); reaching for it to avoid a cast is throwing away the type system.
object o = "hi"; // o.Length; // compile error: object has no Length int len1 = ((string)o).Length; // cast, then call — checked dynamic d = "hi"; int len2 = d.Length; // compiles; resolved at runtime // d.Nope(); // compiles too — throws RuntimeBinderException
Why it's asked / follow-up: it checks that you know dynamic defers checking to runtime rather than being “a nicer object.” Follow-up: “when is dynamic justified?” — interop with dynamically-typed systems; not as a shortcut around generics or casting. dynamic arrived in C# 4.0.
Source: C# guide — Using type dynamic.
What are tuples and deconstruction? Mid
A value tuple ((int, string), backed by the System.ValueTuple struct) bundles several values into one lightweight, value-typed package — the idiomatic way to return more than one value without an out-parameter or a one-off class. Elements can be named ((int id, string name)). Deconstruction unpacks a tuple (or any type with a Deconstruct method) into separate variables. Note these are the modern C# 7 value tuples, not the older reference-typed System.Tuple with its .Item1/.Item2 properties.
(int id, string name) GetUser() => (1, "Ada"); var (id, name) = GetUser(); // deconstruct into two locals var u = GetUser(); // or keep it: u.id, u.name
Why it's asked / follow-up: tuples are everywhere in modern C# and the value-vs-reference tuple split trips people up. Follow-up: “tuple or a record?” — a tuple for a transient, local return; a record when the shape is part of your API and deserves a name. Value tuples and deconstruction arrived in C# 7.0.
Source: C# reference — Tuple types.
2 · Classes, structs & records
What is a record, and how does it differ from a class?
Mid
A record is a class (a reference type, unless you write record struct) for which the compiler synthesizes value-based equality — an Equals/GetHashCode that compare every field, value-semantic ==/!=, a readable ToString, a Deconstruct, and a copy-and-modify with expression. A plain class uses reference equality by default (two distinct objects are unequal even with identical data). Records are aimed at immutable data carriers (DTOs, domain values, messages) where “same contents means equal” is what you want.
record Person(string First, string Last); // positional record var a = new Person("Ada", "Lovelace"); var b = new Person("Ada", "Lovelace"); bool eq = a == b; // true — value equality (a class: false) var c = a with { Last = "Byron" }; // non-destructive copy
Why it's asked / follow-up: records are the headline modern-C# data feature and the equality behavior is the whole point. Follow-up: “record vs record struct?” — both get synthesized value equality and with; record is a reference type, record struct a value type (a struct already has value equality, but the record form generates an efficient one plus ToString/Deconstruct). Records arrived in C# 9; record structs in C# 10.
Source: C# reference — Records.
What are init-only setters and with expressions?
Mid
An init accessor is a setter that can run only during construction or object-initializer syntax, then the property is read-only. It gives you immutability and the convenience of object-initializer syntax — you no longer have to choose between a big constructor and a mutable object. A with expression creates a copy of a record (or struct) with a few properties changed, leaving the original untouched — the idiomatic way to “modify” immutable data.
class Config { public int Port { get; init; } } var c = new Config { Port = 80 }; // ok during initialization // c.Port = 443; // error: init-only after construction
Why it's asked / follow-up: they're the building blocks of records and modern immutable C#. Follow-up: “how is init different from readonly?” — a readonly field is set only in the constructor; an init property is also settable via object-initializer syntax at the call site. Both arrived in C# 9.
Source: C# reference — init / with expression.
What is a property, and what is an auto-property? Junior
A property is a pair of accessor methods (get/set) that look like a field to callers but run code on access — it lets a type expose data while keeping the freedom to validate, compute, or change the backing store later without breaking the API. An auto-implemented property ({ get; set; }) is the shorthand for the trivial case: the compiler generates a hidden backing field. An expression-bodied property (=>) is the shorthand for a computed, read-only one.
class Circle { public double Radius { get; set; } // auto-property public double Area => Math.PI * Radius * Radius; // expression-bodied, computed }
Why it's asked / follow-up: it's a daily-use feature whose “why not just a public field?” answer reveals understanding of encapsulation and binary compatibility. Follow-up: “why prefer a property over a public field?” — you can add validation later, properties work with data binding and interfaces, and swapping a field for a property is a breaking ABI change you avoid by starting with a property.
Source: C# guide — Properties.
What are the access modifiers, and what does internal mean?
Junior
Six levels, from most to least open: public (anywhere), protected (the type and its derived types), internal (anywhere in the same assembly), protected internal (the union — same assembly or derived types), private protected (the intersection — derived types within the same assembly), and private (the containing type only). The one people miss is internal: it's the default for top-level types and is scoped to the assembly (DLL), which is how a library keeps implementation types invisible to consumers while still using them freely internally.
public class Api { } // visible to other assemblies internal class Helper { } // only inside this assembly (the default) class AlsoInternal { } // no modifier on a top-level type ⇒ internal
Why it's asked / follow-up: a fluency check that also surfaces the assembly-scoping concept. Follow-up: “default access for a class member?” — private; for a top-level type it's internal. (And InternalsVisibleTo can open internal members to a named test assembly.)
Source: C# guide — Access modifiers.
3 · Inheritance & polymorphism
virtual/override vs new — what's the difference?
Mid
override is true polymorphism: a virtual (or abstract) base member and an override in the derived class share one slot, so a call through a base-typed reference runs the most-derived implementation. new is method hiding: it declares an unrelated member that merely shares a name, so which one runs depends on the compile-time type of the reference, not the runtime object. new is almost never what you want — it's a footgun that makes behavior depend on how a variable is typed.
class Base { public virtual string Name() => "base"; } class Over : Base { public override string Name() => "over"; } class Hide : Base { public new string Name() => "hide"; } Base o = new Over(); o.Name(); // "over" — virtual dispatch Base h = new Hide(); h.Name(); // "base" — hidden, picks the reference type
Why it's asked / follow-up: the new-hiding output surprises people and proves whether you understand virtual dispatch. Follow-up: “what does sealed do here?” — sealed override stops a further-derived class from overriding again, and sealed class blocks inheritance entirely (which the JIT can sometimes optimize).
Source: C# guide — Override and New keywords.
Abstract class vs interface — when do you use each? Junior
An interface is a pure contract: it says what a type can do, carries no instance state, and a class can implement many. An abstract class is a partially-built base: it can hold fields, constructors, and shared implementation alongside abstract members, but a class can inherit only one. Rule of thumb: interface for “can do X” capabilities you want to mix across unrelated types (IDisposable, IComparable); abstract class for an “is-a” family that shares real code and state.
interface IShape { double Area(); } // contract; implement many abstract class Shape { public string Name = ""; // shared state... public abstract double Area(); } // ...plus a required member
Why it's asked / follow-up: it's the most-asked OOP-design question in C#. Follow-up: “hasn't C# 8 blurred the line with default interface methods?” — somewhat (interfaces can now carry implementation), but interfaces still can't hold instance fields or a constructor, and a type can still implement many interfaces but inherit one class.
Source: C# guide — Interfaces.
What are default interface methods? Senior
Since C# 8 an interface member can ship a default body. The motivating use is API evolution: a library can add a method to a published interface without breaking the existing implementers, because they inherit the default. The catch most people miss: a default-implemented member is callable only through the interface reference, not through the concrete type, unless the class re-declares it.
interface ILogger { void Log(string msg); void LogError(string msg) => Log("ERROR: " + msg); // default body } class ConsoleLogger : ILogger { public void Log(string m) => System.Console.WriteLine(m); } ILogger log = new ConsoleLogger(); log.LogError("x"); // ok via interface // new ConsoleLogger().LogError("x"); // error — not on the class
Why it's asked / follow-up: it's a modern feature with a non-obvious calling rule, good for separating depth. Follow-up: “does this make C# multiple-inheritance?” — of behavior, partially; not of state (interfaces still can't hold instance fields). Arrived in C# 8.
Source: C# guide — Default interface methods.
What is explicit interface implementation, and why use it? Senior
Naming the interface on the member (void IFoo.Bar(), no access modifier) implements it explicitly: the method is accessible only through a reference of that interface type, not through the class's own surface. Two reasons to reach for it: (1) a type implements two interfaces that declare the same member signature with different intended behavior, and (2) you want to keep an interface member off the class's public API (common with IDisposable.Dispose wrappers or legacy IEnumerable.Current).
interface IEnglish { string Greet(); } interface IFrench { string Greet(); } class Bot : IEnglish, IFrench { string IEnglish.Greet() => "Hello"; // disambiguated by interface string IFrench.Greet() => "Bonjour"; } ((IFrench)new Bot()).Greet(); // "Bonjour" — must go through the interface
Why it's asked / follow-up: it's the canonical answer to “what if two interfaces collide?” Follow-up: “can you call an explicit member from inside the class?” — only by casting this to the interface first; that's the deliberate hiding.
4 · Generics
What are generics, and why use them? Junior
Generics let a type or method take a type parameter so it works over many types while staying fully type-safe. Before generics, reusable containers stored object, which meant casts everywhere and boxing for value types; List<T> keeps the element type, so the compiler enforces it and no boxing or casting happens. Unlike Java, C# generics are reified — List<int> is a real distinct type at runtime (so typeof(T) works and there's no type erasure).
T First<T>(IReadOnlyList<T> xs) => xs[0]; int n = First(new[] { 1, 2, 3 }); // T = int — no boxing, no cast string s = First(new[] { "a" }); // T = string — type preserved
Why it's asked / follow-up: generics underpin the entire collections and LINQ surface. Follow-up: “generics vs storing object?” — object loses the type (forcing casts) and boxes value types; a generic preserves the type and the performance.
Source: C# guide — Generics.
How do generic constraints (where T : …) work?
Mid
A constraint tells the compiler what a type parameter is guaranteed to support, which lets you use those capabilities inside the generic. The common ones: where T : class / struct (reference / value type), where T : SomeBase (derives from), where T : IComparable<T> (implements), where T : new() (has a public parameterless constructor), and where T : notnull. Without a constraint, T is treated as object and you can only use object's members.
T Max<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) >= 0 ? a : b; // CompareTo allowed by the constraint T Create<T>() where T : new() => new T(); // new() constraint enables new T()
Why it's asked / follow-up: it's the step from “what is a generic” to “write a useful one.” Follow-up: “why does new T() need a constraint?” — without where T : new() the compiler can't assume T has an accessible parameterless constructor.
What are covariance and contravariance (out / in)?
Senior
Variance is when a generic of a derived type is assignable to a generic of its base type. Covariance (out T) preserves the direction: because IEnumerable<out T> only ever produces T, an IEnumerable<string> is an IEnumerable<object>. Contravariance (in T) reverses it: because Action<in T> only consumes T, an Action<object> can be used where an Action<string> is expected. Variance is allowed only on interfaces and delegates, and only for reference-type arguments.
IEnumerable<string> strings = new[] { "a" }; IEnumerable<object> objects = strings; // covariant (out): string ⇒ object Action<object> printAny = o => System.Console.WriteLine(o); Action<string> printStr = printAny; // contravariant (in): object ⇒ string
Why it's asked / follow-up: it's a genuine depth check; most people use variance daily (LINQ, IComparer) without naming it. Follow-up: “why can't List<T> be covariant?” — it both reads and writes T, so neither in nor out is safe; allowing it would let you put an object into a List<string>.
When is a generic over-engineering? Senior
A type parameter earns its place only when it ties two things together — an argument type to a return type, or one parameter to another. If T appears in a signature exactly once and isn't constrained to do anything, it conveys nothing the base type or object wouldn't, and just makes the API harder to read. The same judgment applies to over-deep constraint chains and generic classes that never vary their parameter.
// Pointless: T appears once, says nothing over `object`. void Log<T>(T x) => System.Console.WriteLine(x); void Log2(object x) => System.Console.WriteLine(x); // same contract // Justified: T links the input type to the output type. T[] Repeat<T>(T x, int n) { var a = new T[n]; Array.Fill(a, x); return a; }
Why it's asked / follow-up: it separates “can write generics” from “knows when not to.” Follow-up: “rule of thumb?” — a type parameter used in only one position is a smell; if it doesn't relate two things, drop it.
Source: C# guide — Generics.
5 · Delegates, events & lambdas
What is a delegate? Mid
A delegate is a type-safe reference to a method — effectively a function pointer with a signature the compiler checks. It lets you pass behavior as a value: store a method in a variable, pass it to another method, return it. Delegates are also multicast — you can combine several with += and invoking the delegate calls them all in order (the basis of events). They're reference types deriving from System.Delegate.
delegate int BinaryOp(int a, int b); // a delegate type BinaryOp op = (a, b) => a + b; // point it at a method (here a lambda) int r = op(2, 3); // 5 — invoke through the delegate
Why it's asked / follow-up: delegates are the foundation under events, LINQ, and callbacks. Follow-up: “do you still declare custom delegate types?” — rarely; Func and Action cover almost every case (next question).
Source: C# guide — Delegates.
Func vs Action vs Predicate — what's the difference?
Mid
They're the built-in generic delegate families, so you almost never declare your own. Action<…> takes parameters and returns void. Func<…, TResult> takes parameters and returns a value — the last type argument is always the return type. Predicate<T> is the older spelling of Func<T, bool> (a test), still seen on APIs like List<T>.FindAll. Modern code mostly uses Func/Action everywhere.
Action<string> print = s => System.Console.WriteLine(s); // returns void Func<int, int, int> add = (a, b) => a + b; // last arg = int return Predicate<int> isEven = n => n % 2 == 0; // == Func<int,bool>
Why it's asked / follow-up: it's a very common, quick fluency check — and people forget that Func's final type parameter is the return type. Follow-up: “Predicate<T> or Func<T,bool>?” — they're interchangeable in meaning; LINQ standardized on Func<T,bool>, so prefer it for consistency.
Source: API — Func<T,TResult> / Action<T>.
What is an event, and how is it different from a public delegate field?
Mid
An event wraps a delegate to implement the publish/subscribe pattern safely. The event keyword restricts outside code to only subscribe (+=) and unsubscribe (-=) — it can't invoke the event or overwrite the whole subscriber list. A bare public delegate field gives callers both powers, so any consumer could raise your event or clobber everyone else's handlers. Only the declaring type can invoke an event.
class Button { public event EventHandler? Clicked; // outsiders may only += / -= public void Press() => Clicked?.Invoke(this, EventArgs.Empty); // only the owner raises it } var b = new Button(); b.Clicked += (s, e) => System.Console.WriteLine("clicked"); // subscribe
Why it's asked / follow-up: it tests whether you know why the event keyword exists rather than just exposing a delegate. Follow-up: “why the ?.Invoke?” — the backing delegate is null when there are no subscribers, so null-conditional invoke avoids a NullReferenceException (and is the thread-safe-read idiom).
Source: C# guide — Events.
What is a closure, and what's the captured-variable trap? Senior
A lambda that uses a variable from its enclosing scope captures the variable, not its value — the compiler lifts that variable into a hidden class shared by the lambda and the surrounding code. So if the variable changes later, the lambda sees the new value. The classic trap is capturing a for-loop counter: every lambda closes over the same variable, so they all observe its final value. (C# 5 fixed this specifically for foreach — each iteration now gets a fresh variable — but a plain for loop still shares one.)
var fs = new List<Func<int>>(); for (int i = 0; i < 3; i++) fs.Add(() => i); // all capture the SAME i // fs[0]() == fs[1]() == fs[2]() == 3 (i's final value) for (int i = 0; i < 3; i++) { int copy = i; fs.Add(() => copy); } // fix: capture a fresh local
Why it's asked / follow-up: the “why do all three print 3” output is a rite-of-passage gotcha. Follow-up: “does foreach have the same bug?” — not since C# 5, which made the foreach iteration variable per-iteration; the for-loop version still bites.
6 · LINQ & iterators
What is LINQ, and what's query syntax vs method syntax? Junior
LINQ (Language-Integrated Query) is a uniform set of operators for querying any sequence — in-memory collections, databases, XML — through the IEnumerable<T> / IQueryable<T> interfaces. There are two surfaces for the same operators: query syntax (the SQL-like from…where…select keywords) and method syntax (chained extension methods like .Where(…).Select(…)). Query syntax is compiled into method calls, so they're equivalent; method syntax exposes more operators (Count, First, Skip) directly.
int[] nums = { 1, 2, 3, 4 }; var q = from n in nums where n % 2 == 0 select n * n; // query syntax var m = nums.Where(n => n % 2 == 0).Select(n => n * n); // method syntax — identical
Why it's asked / follow-up: it confirms you know the two are the same thing compiled differently. Follow-up: “which to use?” — method syntax for most work; query syntax shines for multi-source joins and group by, where it reads more clearly. LINQ arrived in C# 3.0.
Source: C# guide — LINQ.
What is deferred execution, and the multiple-enumeration trap? Senior
Most LINQ operators (Where, Select, Take…) are lazy: calling them builds a query but runs nothing. The query executes only when you enumerate it — a foreach, or a materializing operator (ToList, ToArray, Count, First). Two consequences bite. First, the query re-runs every time you enumerate it, so iterating the same IEnumerable twice does the work twice (and may hit the database twice, or yield different results if the source changed). Second, it captures variables by reference, so changing them before enumeration changes the result.
var query = nums.Where(n => { System.Console.WriteLine("eval"); return n > 1; }); var first = query.Count(); // runs the predicate now var second = query.Count(); // runs it AGAIN — re-enumerated var snapshot = query.ToList(); // materialize once; enumerate snapshot freely
Why it's asked / follow-up: multiple enumeration is a top real-world LINQ performance bug (and your IDE warns about it). Follow-up: “how do you avoid it?” — materialize once with ToList()/ToArray() when you'll enumerate more than once or the source is expensive.
Source: C# guide — Standard query operators (deferred execution).
IEnumerable vs IQueryable — what's the difference?
Mid
Both represent a queryable sequence, but they run in different places. IEnumerable<T> is LINQ-to-Objects: the query runs in your process, in memory, with the predicate as compiled delegates. IQueryable<T> holds the query as an expression tree that a provider (Entity Framework, etc.) translates into something else — SQL run on the database server. The practical trap: once you call an IEnumerable operator (or .AsEnumerable()) on an IQueryable, the rest of the query is pulled into memory and runs client-side — potentially fetching the whole table first.
// EF: translated to SQL, filtered on the DB — only matching rows come back IQueryable<User> q = db.Users.Where(u => u.Age > 18); // .AsEnumerable() switches to in-memory: the Where now runs client-side var bad = db.Users.AsEnumerable().Where(u => u.Age > 18); // fetches ALL users first
Why it's asked / follow-up: getting this wrong silently turns a fast indexed query into “load the table, filter in C#.” Follow-up: “what happens if a provider can't translate your expression?” — it throws at execution (EF Core), or older stacks silently evaluate client-side; keep server-side queries to translatable expressions.
Source: API — IQueryable<T>.
What does yield return do?
Mid
yield return turns a method into an iterator: the compiler builds a state machine so the method produces values lazily, one at a time, pausing its execution between each. The method body doesn't run until the result is enumerated, and it runs only as far as each MoveNext demands — which is how you can iterate (or even produce) an infinite sequence and stop early. It's the same deferred-execution model LINQ is built on.
IEnumerable<int> Naturals() { int n = 1; while (true) { yield return n++; } // produced lazily, on demand } foreach (var x in Naturals().Take(3)) { } // 1, 2, 3 — then stops
Why it's asked / follow-up: it reveals whether you understand lazy enumeration and the compiler-generated state machine. Follow-up: “when does the code before the first yield run?” — not at call time, but on the first MoveNext — a reason argument validation in an iterator method is often split into a non-iterator wrapper.
Source: C# reference — yield.
First vs FirstOrDefault vs Single — which throws?
Mid
They differ on the empty / multiple cases. First returns the first match and throws if there are none. FirstOrDefault returns the default (null for reference types, 0 for int, etc.) instead of throwing on empty. Single demands exactly one match — it throws if there are zero or more than one — and SingleOrDefault allows zero but still throws on more than one. Pick by intent: Single when “exactly one” is an invariant you want enforced; First when “any will do, take the first.”
nums.First(n => n > 2); // 3; throws if none match nums.FirstOrDefault(n => n > 9); // 0 (default int); no throw nums.Single(n => n == 2); // 2; throws if 0 or 2+ match
Why it's asked / follow-up: the throw-vs-default behavior is a frequent source of production exceptions. Follow-up: “FirstOrDefault on a value-type sequence returned 0 — is that ‘found’ or ‘not found’?” — ambiguous; that's why for non-null value types you often want Cast<int?>() or an explicit .Any() check first.
7 · Async & await
What do async and await actually do?
Mid
await does not block a thread. When you await a not-yet-complete Task, the method suspends, returns control to its caller, and registers the rest of itself (the continuation) to resume when the task finishes — freeing the current thread to do other work meanwhile. async is just the marker that lets a method contain await and have the compiler rewrite it into that state machine; on its own it adds no concurrency. The win is scalability: a web server can have thousands of requests “awaiting” I/O without thousands of blocked threads.
async Task<string> FetchAsync(HttpClient http) { string body = await http.GetStringAsync("/api"); // suspends; thread is freed return body.Trim(); // continuation, after I/O completes }
Why it's asked / follow-up: the “does await start a new thread?” misconception is extremely common. Follow-up: “is async multithreading?” — no; it's about not blocking on I/O. No new thread is created for the wait; a thread-pool thread may run the continuation. async/await arrived in C# 5.
Source: C# guide — Asynchronous programming.
Task vs Task<T> vs ValueTask — when each?
Mid
Task is an asynchronous operation that returns no value; Task<T> is one that produces a T. Both are reference types, so each async call allocates one. ValueTask<T> is a struct that avoids that allocation in the common case where the result is often already available synchronously (a cache hit, a buffered read) — a hot-path optimization. The cost: a ValueTask has sharp rules — you may await it only once, and must not block on it or use it concurrently. Default to Task; reach for ValueTask only when profiling shows the allocation matters.
async Task SaveAsync() { /* ... */ } // no result async Task<int> CountAsync() => 42; // result of T ValueTask<int> ReadAsync() => _cached is int v ? new ValueTask<int>(v) // synchronous: no Task allocation : new ValueTask<int>(SlowAsync());
Why it's asked / follow-up: it probes whether you know ValueTask is an optimization with constraints, not a free upgrade. Follow-up: “why not use ValueTask everywhere?” — the await-once / don't-block rules make it error-prone, and for genuinely-async paths it can be slightly slower than Task.
Source: API — ValueTask<TResult>.
Why is async void dangerous?
Senior
An async void method returns nothing awaitable, which breaks two things. You can't await it, so callers can't know when it finished or compose it. And an exception thrown inside it can't propagate to a caller — it's raised on the synchronization context and typically crashes the process rather than being catchable. The one legitimate use is an event handler, whose signature must be void. Everywhere else, return Task (or Task<T>) so the call is awaitable and its failures are observable.
async void FireAndForget() { throw new Exception(); } // ⇒ unobserved; may crash async Task DoWorkAsync() { throw new Exception(); } // awaitable; catchable button.Click += async (s, e) => await DoWorkAsync(); // the OK async void: a handler
Why it's asked / follow-up: it's the single most-cited async anti-pattern. Follow-up: “what's the rule?” — “async all the way, and async void only for event handlers.”
Source: MSDN Magazine — Async/Await Best Practices (Cleary).
What causes the .Result / .Wait() deadlock, and what's ConfigureAwait(false)?
Senior
Blocking on an async call with .Result or .Wait() from a thread that has a single-threaded synchronization context (a classic ASP.NET request thread, or a WPF/WinForms UI thread) deadlocks: by default the awaited continuation tries to resume on that same context, but the context's only thread is blocked waiting for the task to finish — so neither can proceed. ConfigureAwait(false) tells await not to capture the context, letting the continuation run on a thread-pool thread — which both avoids this deadlock and saves a context hop in library code.
// Deadlocks on a UI / classic-ASP.NET thread: var x = FetchAsync().Result; // blocks the context thread the continuation needs // Fix the real way — stay async: var x = await FetchAsync(); // In library code, don't capture the context: var body = await http.GetStringAsync(url).ConfigureAwait(false);
Why it's asked / follow-up: it's the deepest of the everyday async traps and tests real understanding of the continuation model. Follow-up: “do you need ConfigureAwait(false) in ASP.NET Core?” — less so; ASP.NET Core has no synchronization context, so the deadlock doesn't arise — but it's still good practice in reusable libraries that might run anywhere.
8 · Pattern matching & expressions
What is a switch expression, and what patterns can it use? Mid
A switch expression (C# 8) is the expression form of switch: each arm is pattern => value, it returns a value, and the compiler warns if the arms aren't exhaustive. It composes with the pattern family that has grown across releases: type patterns (int n), constant, relational (> 0), logical (and/or/not), property ({ Length: 0 }), and positional patterns on deconstructable types. The _ arm is the discard (default).
string Describe(int n) => n switch { < 0 => "negative", // relational pattern 0 => "zero", // constant pattern > 0 and < 10 => "small", // logical pattern _ => "large" // discard / default };
Why it's asked / follow-up: pattern matching is the signature modern-C# expressiveness gain and interviewers probe how much of the family you know. Follow-up: “what happens if no arm matches?” — a SwitchExpressionException at runtime, which is why the exhaustiveness warning matters. Patterns began in C# 7 and expanded through C# 9.
Source: C# guide — Pattern matching.
is vs as vs a cast — what's the difference?
Junior
Three ways to deal with a runtime type, with different failure behavior. A cast (T)x converts and throws InvalidCastException if it can't. as attempts the conversion and returns null on failure (reference and nullable types only) — no exception, so you check for null. is returns a bool, and the modern form binds the result to a variable in one step (x is T t), which has replaced the old “is then cast” double-check.
object o = "hi"; string a = (string)o; // cast: throws if o isn't a string string? b = o as string; // null if not a string; no throw if (o is string s) { /* s is the narrowed string */ } // test + bind
Why it's asked / follow-up: choosing the wrong one is a frequent bug (an unguarded as followed by a deref is a delayed NullReferenceException). Follow-up: “when prefer a cast over as?” — when a wrong type is a bug you want to fail loudly; as is for “might legitimately not be this type.”
What do ?., ??, and ??= do?
Junior
The null-handling operators. Null-conditional ?. (and ?[]) short-circuits the whole expression to null if the left side is null, instead of throwing — so a?.B?.C is null if any link is null. Null-coalescing ?? supplies a fallback when the left is null (x ?? fallback). Null-coalescing assignment ??= assigns only if the target is currently null (x ??= new()) — the lazy-initialize idiom.
int? len = name?.Length; // null if name is null (no NRE) string shown = name ?? "(none)"; // fallback when name is null _cache ??= new List<int>(); // assign only if _cache is null
Why it's asked / follow-up: they're everywhere in idiomatic C# and people conflate ?. with ??. Follow-up: “what does a?.b() return when a is null and b() returns a value type?” — the result is lifted to a nullable, so it returns null, not the value type's default. ?. arrived in C# 6, ??= in C# 8.
What are nameof and target-typed new?
Mid
Two small expression-level conveniences. nameof(x) compiles to the string "x" — a refactor-safe way to name a parameter, property, or member, so renaming via tooling keeps your exception messages and INotifyPropertyChanged calls correct (no stale string literal). Target-typed new (C# 9) lets you write new() when the type is already known from the left-hand side, cutting the repetition out of declarations and field initializers.
void Set(string id) { if (id is null) throw new ArgumentNullException(nameof(id)); // "id", refactor-safe } List<int> xs = new(); // target-typed: type inferred from the declaration Dictionary<string, int> m = new();
Why it's asked / follow-up: both show fluency with modern, readable C#. Follow-up: “why nameof over a string literal?” — the compiler checks the symbol exists and renames follow it; a literal silently rots. nameof arrived in C# 6, target-typed new in C# 9.
Source: C# reference — nameof.
9 · Nullability, exceptions & resources
What are nullable reference types and the ! operator?
Mid
Nullable reference types (NRTs, C# 8, opt-in via <Nullable>enable</Nullable>) are a compile-time flow-analysis feature: with it on, string means “shouldn't be null” and string? means “might be,” and the compiler warns when you might dereference a null or assign null to a non-nullable. It changes no runtime behavior — nothing stops a null arriving from un-annotated or external code. The null-forgiving operator ! tells the compiler “trust me, this isn't null here,” suppressing the warning without any runtime check — so a wrong ! just defers the NullReferenceException.
string notNull = "hi"; // must not be null string? maybe = null; // may be null // int n = maybe.Length; // warning CS8602: possible null dereference int n = maybe!.Length; // ! suppresses the warning — no runtime guard
Why it's asked / follow-up: people assume NRTs enforce non-null at runtime — they don't. Follow-up: “when is ! appropriate?” — rarely, when you genuinely know more than the analyzer (e.g. after a custom assert); prefer a real null check. NRTs arrived in C# 8.
Source: C# guide — Nullable reference types.
How do try/catch/finally and exception filters (when) work?
Mid
catch handles a matching exception type; finally always runs — on success, on a handled throw, or as the stack unwinds — so it's where you put cleanup. An exception filter (catch (E e) when (cond), C# 6) only enters the catch when cond is true; crucially the filter is evaluated without unwinding the stack, so if it's false the exception continues to propagate as if this catch weren't there — better for diagnostics than catching, inspecting, and re-throwing.
try { Process(); } catch (HttpRequestException e) when (e.StatusCode == 503) // only this case { Retry(); } finally { cleanup(); } // runs regardless of outcome
Why it's asked / follow-up: the when filter and its no-unwind behavior separate working knowledge from surface knowledge. Follow-up: “filter vs catch-and-rethrow?” — a filter leaves the stack intact when it declines, so the original throw point is preserved; catching then throw; still unwinds into the catch first.
What do using and IDisposable do?
Mid
IDisposable is the contract for deterministic cleanup of unmanaged or expensive resources (file handles, sockets, DB connections) — the garbage collector reclaims memory but won't promptly release those, so you call Dispose(). A using statement guarantees Dispose() runs when the block exits, even on an exception — it's try/finally sugar. The using declaration (C# 8, no braces) disposes at the end of the enclosing scope, and await using handles IAsyncDisposable for resources whose cleanup is itself async.
using (var f = File.OpenRead(path)) { Read(f); } // Dispose() at block end using var conn = new SqlConnection(cs); // declaration: disposes at scope end await using var stream = GetAsyncStream(); // IAsyncDisposable
Why it's asked / follow-up: resource leaks (undisposed connections, file locks) are a classic production problem and using is the fix. Follow-up: “does Dispose free memory?” — no; the GC frees managed memory. Dispose releases non-memory resources promptly and deterministically. using declarations and IAsyncDisposable arrived in C# 8.
Source: .NET guide — Using objects that implement IDisposable.
What's the difference between throw; and throw ex;?
Mid
Inside a catch, a bare throw; re-throws the current exception and preserves its original stack trace — the place it was actually raised. throw ex; throws the same object but resets the stack trace to the re-throw line, erasing where the failure really originated and making the bug far harder to find in logs. Almost always you want bare throw; (or wrap in a new exception with the original as InnerException).
try { DeepCall(); } catch (Exception ex) { Log(ex); throw; // keeps the original stack trace ✓ // throw ex; // resets it to HERE — loses the origin ✗ }
Why it's asked / follow-up: it's a small detail with a big debugging cost, and a reliable senior-vs-junior tell. Follow-up: “how do you add context without losing the trace?” — throw new MyException("context", ex); — the inner exception keeps the original trace; or use ExceptionDispatchInfo to re-throw a captured exception across stack frames.
10 · Classic C# gotchas
== vs .Equals() vs ReferenceEquals — and what about strings?
Senior
For reference types, == defaults to reference identity (same object?) unless the type overloads it; .Equals() is virtual, so an overriding type gives it value semantics; object.ReferenceEquals(a, b) always tests identity and can't be overridden. The classic curveball is string: it overloads == and overrides Equals to compare characters, so both give value equality — but ReferenceEquals on two strings may be true or false depending on interning. Compile-time string literals are interned (so they share one reference), while strings built at runtime are not.
string a = "hi", b = "hi"; // literals — interned string c = new string(new[]{'h','i'}); // built at runtime a == c; // true — value equality Object.ReferenceEquals(a, b); // true — both point at the interned "hi" Object.ReferenceEquals(a, c); // false — c is a distinct object
Why it's asked / follow-up: it bundles three equality concepts plus interning, all common bug sources. Follow-up: “if you override Equals, what else must you do?” — override GetHashCode to stay consistent (equal objects must hash equal), or hash-based collections break.
Source: C# guide — Equality comparisons.
What are boxing and unboxing, and why care? Mid
Boxing wraps a value type in a heap-allocated object so it can be treated as a reference (assigned to object or an interface); unboxing copies the value back out (with a type check). The reason it matters is performance: each box is a heap allocation plus a copy, and in a hot loop that's GC pressure that's invisible in the source. Generics largely eliminated the everyday cause — List<int> stores int directly, where the old non-generic ArrayList boxed every element.
int n = 42; object boxed = n; // boxing: heap allocation + copy int back = (int)boxed; // unboxing: type-checked copy out // Hidden boxing: passing a struct where an interface is expected, // or string.Format("{0}", n) — each value-type arg gets boxed.
Why it's asked / follow-up: boxing is the “invisible allocation” behind surprising GC pressure. Follow-up: “name a sneaky boxing source” — calling a non-overridden object method on a struct, using a struct through a non-generic interface, or value-type args to params object[] APIs.
Source: C# guide — Boxing and unboxing.
Why are mutable structs a trap? Senior
Because a struct is copied on access, mutating one you got from a property, a collection, or a non-ref local often mutates a throwaway copy while the original is unchanged. A property getter returns a copy, so obj.Position.X = 5 writes into a temporary (and the compiler actually blocks that case); reading a struct out of a List by index gives a copy too. This is why the guidance is to make structs readonly and immutable — then there's nothing to mutate by accident.
struct Counter { public int N; public void Inc() => N++; } var list = new List<Counter> { new Counter() }; list[0].Inc(); // mutates a COPY returned by the indexer; list[0].N stays 0 var arr = new Counter[1]; arr[0].Inc(); // arrays are different: this DOES update arr[0].N → 1
Why it's asked / follow-up: the “why didn't my list element change?” bug is a direct consequence of value semantics. Follow-up: “why does the array version work but the list doesn't?” — array element access yields a direct reference to the element; List<T>'s indexer is a property that returns a copy.
Why do all my loop-created lambdas print the same value? Senior
Because a closure captures the variable, not a snapshot of its value — and a classic for loop has one counter variable shared by every iteration. All the lambdas close over that same variable, so when they run later they all read its final value. The fix is to capture a fresh per-iteration local. C# 5 made exactly this fix for foreach (the iteration variable became per-iteration), but the for-loop form was deliberately left alone — so the gotcha still lives in for loops and in any code that captures a single mutable variable across iterations.
var actions = new List<Action>(); foreach (var s in new[]{"a","b"}) actions.Add(() => Print(s)); // "a","b" since C# 5 for (int i = 0; i < 2; i++) actions.Add(() => Print(i)); // 2,2 — shared i
Why it's asked / follow-up: it's the canonical “explain this output” question, and the foreach-vs-for asymmetry catches even experienced devs. Follow-up: “why wasn't for changed too?” — the for variable is genuinely a single declaration with explicit scope; changing its semantics would be more surprising than the foreach fix was. See the C# 5 foreach change.
Source: C# reference — Lambda expressions (variable capture).
What is default(T), and what surprises come from it?
Mid
Every type has a default value used for unassigned fields, array elements, and default(T) / default: 0 for numbers, false for bool, '\0' for char, null for any reference type, and an all-fields-default instance for a struct (so a struct is never null — it always has a usable zeroed value). The surprises: a FirstOrDefault over an int sequence returns 0, which is indistinguishable from a real 0; and a DateTime field you forgot to set is 0001-01-01, not null and not “now.”
int i = default; // 0 bool b = default; // false string? s = default; // null (reference type) DateTime d = default; // 0001-01-01, NOT DateTime.Now
Why it's asked / follow-up: default-value confusion drives subtle bugs (a “missing” value that's really 0 or the min date). Follow-up: “can a struct be null?” — no; only a nullable value type (T? / Nullable<T>) can, which is exactly why int? exists. The default literal arrived in C# 7.1.
Every answer links its primary source inline — the official C# documentation and language reference, with the C# language specification (ECMA-334) where an answer needs spec-level provenance. The questions are a curated set of the topics a C# interviewer commonly covers, not a copy of any question bank. This page covers the C# language; the CLR / .NET runtime it runs on (garbage collection, the JIT, the thread pool) is the .NET version reference's territory. Feature-arrival details cross-link to the C# version reference. Last updated June 2026.
Mungomash LLC · More on C#