C# Interview Questions
Real C# interview questions and answers covering record vs class, value vs reference types, async/await and LINQ.
A curated set of C# interview questions, ordered roughly from fundamentals to more advanced — the kind you'll actually be asked in real screens and on-sites.
Language fundamentals
Q: What's the difference between a value type and a reference type?
A value type (struct, int, bool, and other built-in numeric types) holds its data directly, and assigning it to another variable copies that data — the two variables are then completely independent. A reference type (class, string, arrays, collections) holds a reference to data on the heap, so assigning it only copies the reference — both variables end up pointing at, and able to mutate, the exact same underlying object.
Q: What's the difference between record and class?
A record is designed for immutable data and gets value-based equality (==/.Equals() compare property values, not references), an auto-generated readable ToString(), and non-destructive copying via with expressions — all generated by the compiler. A class defaults to reference equality and gives you none of that for free; you'd need to hand-write Equals, GetHashCode, and ToString() to match a record's behavior. Use record for data models and DTOs; use class for objects with identity and mutable behavior.
Q: What problem do nullable reference types solve?
Enabled via <Nullable>enable</Nullable>, they let the compiler statically distinguish references that can be null (string?) from those that are guaranteed non-null (string), flagging with a warning any code path where you might dereference something that could be null. This catches a large share of NullReferenceException bugs at compile time instead of at runtime in production — though it remains a compile-time analysis, not a hard runtime guarantee.
OOP
Q: Why does C# require virtual before a method can be overriden?
Unlike Java, where methods are overridable by default unless marked final, C# requires the base class to explicitly opt a method into being overridable with virtual. This is a deliberate design choice: every override relationship in a codebase is intentional and visible right at the base class declaration, and it lets the JIT compiler safely devirtualize (inline) calls to non-virtual methods for better performance.
Q: What's the difference between an interface and an abstract class in C#?
An interface defines a contract only — traditionally no implementation at all, though modern C# interfaces can include default method bodies. An abstract class can hold real field state and concrete method implementations alongside abstract members that subclasses must implement. A class can implement any number of interfaces but can only inherit from one base (abstract or concrete) class.
Async and LINQ
Q: What actually happens under the hood when you await a Task?
The compiler transforms an async method into a state machine. When execution hits an await, if the awaited Task isn't already complete, the method returns control to its caller immediately — freeing the current thread rather than blocking it — and registers a continuation. When the Task completes, that continuation resumes the method's remaining code, typically on a thread-pool thread (or the captured synchronization context in UI applications).
Q: What does it mean that LINQ queries use deferred execution?
A LINQ query built with .Where(), .Select(), and similar methods only describes the query — it isn't actually run until the result is enumerated, via a foreach loop or a terminal call like .ToList() or .Count(). This means the query executes against the collection's state at enumeration time, so if the underlying collection changes between building the query and enumerating it, the results reflect the updated data — and enumerating the same query object twice runs the underlying logic twice.
Exception handling
Q: What does an exception filter (catch (SomeException ex) when (condition)) do differently from catching the exception and checking the condition with an if inside the block?
With a when filter, the clause only actually catches the exception if the condition is true; if it's false, the runtime treats that clause as not matching and moves on to the next catch clause (or propagates the exception further if nothing else matches). Catching unconditionally and branching with if inside the block, by contrast, always catches the exception regardless of the condition — potentially handling (and thereby swallowing) a case that a later, more appropriate catch clause should have handled instead.
Q: Why must more specific exception types be caught before more general ones in the same try statement?
catch clauses are evaluated top to bottom, and the first one whose type matches (including matching a base type of the thrown exception) is the one that runs — so a general clause listed first would always match before a more specific one gets a chance to, making the specific clause unreachable. When the exception types involved are in a direct inheritance relationship, the C# compiler actually flags this ordering as a compile-time error rather than letting it become a silent runtime bug.
Collections and LINQ
Q: When would you reach for Dictionary<TKey, TValue> instead of List<T>?
When lookups need to happen by some unique identifier rather than by position — a Dictionary gives O(1) average-case lookup, insertion, and removal by key, versus List<T>.Find/Contains, which are O(n) linear scans. List<T> remains the right choice when order matters, duplicates are allowed, or the data is mostly accessed by index or iterated through in full rather than looked up by key.
Q: What's a common pitfall from LINQ's deferred execution involving a captured variable?
If a LINQ query's lambda captures a local variable, and that variable's value changes after the query is built but before it's enumerated, the query uses the variable's value at enumeration time, not at the time the query was written — which can produce results that look wrong if you expected the query to have "locked in" the variable's value up front. Calling .ToList() or .ToArray() immediately after building the query forces it to execute right away, capturing a fixed snapshot that's unaffected by anything that happens to the captured variable or the source collection afterward.
Testing
Q: What's the difference between xUnit's [Fact] and [Theory] attributes?
[Fact] marks a test method representing one single, fixed scenario with no parameters. [Theory], combined with [InlineData] (or [MemberData] for non-constant data), runs the same test method once per supplied row of data, with each row reported as its own individually named passing or failing result — the standard way to check the same logic against several different inputs without duplicating the test method itself.
Q: How does xUnit isolate state between test methods in the same test class?
xUnit creates a brand-new instance of the test class for every individual test method, so setup performed in the constructor reruns fresh before each test — no state persists between tests by default, with no separate [SetUp] attribute needed. If the class implements IDisposable, its Dispose() method runs after each test as the matching per-test teardown step.