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

Junior — expected of anyone writing C#; the everyday syntax and types.
Mid — the language in practice: generics, LINQ, async, records, and the “why.”
Senior — value semantics, variance, deferred execution, the async pitfalls, and the gotchas.
Difficulty

Showing 0 of 0 questions

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.

Source: C# guide — Implicitly typed local variables.

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.

Source: C# guide — Explicit interface implementation.

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 reifiedList<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.

Source: C# guide — Constraints on type parameters.

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>.

Source: C# guide — Covariance and contravariance.

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.

Source: C# reference — Lambda expressions (capture).

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.

Source: API — Enumerable.First / FirstOrDefault / Single.

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.

Source: .NET Blog — ConfigureAwait FAQ (Toub).

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.”

Source: C# reference — Type-testing and cast operators.

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.

Source: C# reference — Null-conditional operators.

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.

Source: C# reference — Exception-handling statements.

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.

Source: .NET guide — Best practices for exceptions.

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.

Source: C# reference — Struct types (design limitations).

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.

Source: C# reference — Default values of C# types.

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#

Last refreshed 2026-06-24 by Charon — new page: C# interview questions.