← All languages
C# interview questions
40 curated C# concept questions with model answers. Practice them out loud in the scored Fundamentals room.
C# interviews often center on the .NET runtime and the async model, since both shape everyday application code. Expect value vs reference types, async/await and Task, LINQ's deferred execution, and the IDisposable pattern.
- Value types vs reference types: struct vs class, boxing and unboxing, and why structs should stay small and immutable.
- async/await and Task: how the state machine works, why you should not block on .Result, and what ConfigureAwait changes.
- IDisposable, using, and the finalizer: deterministic cleanup versus garbage collection, and how to combine both safely.
What interviewers are really testing
C# interviewers focus on the .NET runtime and the async model because both shape everyday code. They want to know whether you understand value versus reference semantics, how async/await maps onto a compiler-generated state machine, and why IDisposable still exists when the garbage collector handles memory for you.
Basic
What is the difference between value types and reference types?
- Value types (struct, int, double, bool, enum) hold their data directly; assignment copies the value.
- Reference types (class, string, array, delegate) hold a reference to the heap; assignment copies the reference, so both names share one object.
- Structs are value types; classes are reference types.
- string is a reference type but immutable, so it behaves like a value in practice.
- Boxing wraps a value type as an object on the heap and is a performance cost in hot paths.
Why use StringBuilder instead of string concatenation?
- string is immutable; each concatenation allocates a new string and copies both sides.
- Building a large string in a loop with += is O(n^2) in total characters copied.
- StringBuilder maintains a mutable buffer, so appends are amortized O(1).
- Use the plain + operator for a fixed number of concatenations, or string interpolation for readability.
- StringBuilder also supports Append, AppendLine, Insert, Replace, and a final ToString().
When do you choose a struct over a class?
- Structs are value types with copy semantics; classes are reference types with identity.
- Good structs are small, immutable, short-lived, and logically a single value (e.g. DateTime, a Point).
- Structs cannot inherit but can implement interfaces; they have no destructor and cannot be null unless nullable.
- Large structs copied often hurt performance; consider `in`/`ref readonly` for read-only passing.
- A mutable struct is a common bug source because copies don't observe mutations.
What's the difference between a field and a property?
- A field is a raw variable; a property is a member exposing get/set accessors backed (usually) by a field.
- Auto-properties ({ get; set; }) let the compiler create the backing field.
- Properties can validate, compute, or restrict access (private set, init-only).
- Properties are the public API convention; fields should generally be private.
- init-only setters allow object-initializer construction without full mutability.
Is `var` the same as `dynamic`?
- var is compile-time type inference: the compiler infers the static type from the initializer and enforces it.
- dynamic defers all type checking to runtime and boxes the call through the DLR — no IntelliSense, slower.
- Use var to avoid repetition when the type is obvious from the right-hand side; use explicit types when it aids readability.
- dynamic is for interop/COM/dynamic scenarios, not general use.
- var cannot be a parameter/return type; dynamic can.
Interface vs abstract class in C# — when each?
- A class can implement many interfaces but extend only one class.
- An abstract class can have state, constructors, and implemented members; an interface (pre-C# 8) only declared a contract.
- C# 8+ interfaces can have default implementations, blurring the line, but they cannot hold instance state.
- Use an interface to describe capability across unrelated types; use an abstract class to share common code.
- Interfaces are essential for dependency injection and mocking.
What is LINQ and where does it execute?
- LINQ gives a consistent query syntax/API (Where, Select, OrderBy, GroupBy) over in-memory collections and IQueryable sources.
- LINQ to Objects uses IEnumerable and runs in-process; LINQ to Entities (EF Core) builds an expression tree translated to SQL.
- Query execution is deferred until enumeration; that is why a query built but never iterated does nothing.
- ToList/ToArray force execution and materialize results, often needed before reusing a query.
- Because IQueryable is translated, some C# expressions cannot be translated and cause runtime errors.
How does `using` relate to IDisposable?
- `using` guarantees Dispose() is called when the scope exits, even if an exception is thrown.
- IDisposable is the standard contract for releasing unmanaged resources (files, connections, handles).
- using declarations (C# 8) dispose at the end of the enclosing scope without a block.
- Disposable objects should also be disposed in a try/finally equivalent, which using implements.
- Finalizers are a safety net and should not replace Dispose.
What are nullable value types and nullable reference types?
- Nullable value types (int?) wrap a value type so it can also be null; use .HasValue/.Value or the ?? operator.
- Nullable reference types (? annotations) are a compile-time feature that warns about possible null dereferences.
- The null-forgiving operator (!) suppresses the warning when you know better.
- The compiler uses flow analysis (e.g. after an `is null` check) to reduce warnings.
- NRTs do not add runtime checking; they are static analysis.
What's the difference between const, readonly, and static?
- const is a compile-time constant baked into callers; it is implicitly static and limited to primitives/strings.
- readonly is a runtime field set once in a declaration or constructor, allowing any type.
- Changing a const requires recompiling every assembly that referenced it; readonly avoids that.
- static belongs to the type, not an instance, and is initialized once.
- A static readonly field is a common pattern for shared immutable instances.
What does the params keyword do?
- params T[] lets a method take a variable number of arguments.
- The compiler packs the arguments into an array, allocating when none are pre-existing.
- It must be the last parameter, and only one params parameter is allowed.
- Calling with an array directly avoids the wrapper allocation.
- Modern overloads often use ReadOnlySpan<T> for zero-allocation variadics.
What do ?. and ?? do?
- ?. short-circuits a member access or call when the receiver is null, yielding null instead of throwing.
- ?[] is the indexer form.
- ?? returns the right operand when the left is null.
- ??= assigns only when the target is null.
- ?. returns a nullable type, so combine with ?? for a default.
What is a [Flags] enum?
- Combining [Flags] with powers-of-two values lets one enum hold a bitmask of options.
- Combine with |, test with HasFlag or bitwise &.
- ToString prints a comma-separated list when [Flags] is present.
- Forgetting [Flags] (or non-power-of-two values) breaks combinations.
- The underlying type can be byte/int/long to control size.
How does string interpolation work?
- $-prefixed strings embed expressions in { } and compile to string.Format.
- Format specifiers and alignment: {value:C}, {value,10}.
- Verbatim interpolated strings use $@"..." for multiline paths.
- In hot paths, interpolated string handlers / StringBuilder avoid allocations.
- Escaping a literal brace uses {{ and }}.
When do you use List, Dictionary, or HashSet?
- List<T> is an ordered, indexable, resizable sequence allowing duplicates.
- Dictionary<TKey,TValue> maps keys to values with O(1) average lookup.
- HashSet<T> stores unique values with O(1) membership and set operations.
- Use SortedDictionary/SortedSet when you need ordering; queue/stack for FIFO/LIFO.
- Choose by the operations you need, and program to interfaces (IList, IDictionary).
How do extension methods work?
- A static method in a static class with `this` on the first parameter appears as an instance method.
- They let you add behavior to types you don't own (including interfaces and BCL types).
- Resolution prefers real instance methods over extensions.
- They are compile-time sugar and can't access private members.
- LINQ is built from extension methods on IEnumerable<T>.
What are partial classes and partial methods?
- partial splits a class/struct/interface across files that the compiler merges.
- Tooling (designers, source generators) uses it to keep generated code separate.
- partial methods let a generated file declare a hook the developer may implement.
- All parts must agree on modifiers like sealed/abstract and accessibility.
- They don't create separate types at runtime.
What are exception filters (catch ... when)?
- catch (Ex e) when (condition) only catches when the condition is true.
- If the condition is false, the search for a handler continues up the stack.
- Filters run before unwinding, so the stack is preserved and logging can be side-effect-free.
- They keep catch blocks focused and avoid rethrow-with-if patterns.
- A filter that itself throws is treated as false.
What are value tuples?
- (int, string) is a ValueTuple struct with value equality and no heap allocation.
- Elements can be named: (int Count, string Name).
- Deconstruct with var (count, name) = pair; or by position.
- They are good for small transient groupings but poor as public APIs that need meaning.
- Tuple equality compares element-wise in order.
What's the difference between virtual and abstract methods?
- virtual provides a base implementation that derived classes may override.
- abstract declares a method with no body; derived classes must override it.
- A class with any abstract member must itself be abstract and cannot be instantiated.
- Overrides use the override keyword; hiding without it warns and is usually a bug.
- abstract members can be combined with abstract properties and events.
Advanced
How do async and await work under the hood?
- async marks a method that can await; await suspends the method and returns control to the caller without blocking a thread.
- The compiler rewrites the method into a state machine that resumes on the captured context.
- An async method returns Task/Task<T>/ValueTask; async void is only for event handlers.
- ConfigureAwait(false) avoids capturing the synchronization context in library code; the UI context is captured by default.
- I/O-bound work should be async; CPU-bound work should not be faked with Task.Run in server code.
What's the difference between a delegate and an event?
- A delegate is a type-safe function pointer; Func/Action are built-in generic delegates.
- An event is a delegate field with restricted access: only the declaring type can invoke or reset it.
- Events implement the observer/publish-subscribe pattern and prevent external code from wiping subscribers.
- Subscribers must unsubscribe to avoid leaks when the publisher outlives them.
- Multicast delegates chain handlers and invoke them in order.
How does garbage collection interact with IDisposable?
- The GC is a generational, tracing collector; it frees managed memory when objects are unreachable, but at an unpredictable time.
- It does not free unmanaged resources (file handles, sockets) promptly — hence IDisposable.
- The Dispose pattern with a finalizer handles both: Dispose releases managed and unmanaged resources; the finalizer is a fallback.
- Suppress finalization (GC.SuppressFinalize) once Dispose runs, to avoid a second collection pass.
- Large object heap allocations and pinning affect GC performance.
What are generic constraints and why use them?
- Constraints (where T : class, struct, new(), IComparable, Base) restrict what types a generic can accept.
- They let the generic body call members the constraint guarantees (e.g. CompareTo with IComparable).
- new() enables `new T()`; class/struct constrain to reference/value types.
- C# generics are reified: type information exists at runtime, unlike Java's erasure.
- Constraints document requirements and produce compile-time errors instead of runtime failures.
What is deferred execution and why does it matter?
- IEnumerable queries are lazy: nothing runs until the sequence is enumerated.
- Each enumeration re-executes the query, which can cause repeated database calls or stale/duplicated results.
- Materialize with ToList/ToArray/ToDictionary when you need a stable snapshot or to avoid re-querying.
- yield return methods also defer and stream, evaluating one element at a time.
- Captured variables in a query reflect their value at enumeration time, not build time.
Explain ref, out, and in parameters.
- ref passes by reference; the argument must be initialized before the call and can be read and written.
- out passes by reference but the callee must assign it; the caller need not initialize it (classic TryX pattern).
- in passes by reference read-only, avoiding a copy for large structs without allowing mutation.
- `ref readonly` returns/provides a read-only reference.
- ref struct types (Span<T>) have stack-only restrictions.
How does `yield return` work?
- A method with yield return becomes a compiler-generated state machine implementing IEnumerable/IEnumerator.
- It produces values lazily, one per MoveNext, without building the whole collection.
- State (locals and position) is preserved between iterations, like a coroutine.
- It enables streaming large or infinite sequences in constant memory.
- The generated IDisposable lets early break/foreach dispose the iterator and run finally blocks.
What is boxing and why is it a performance concern?
- Boxing wraps a value type in an object on the heap; unboxing extracts it with a cast.
- It allocates and adds GC pressure and a copy, so it hurts in hot loops.
- Common triggers: storing a value in a non-generic collection (ArrayList), string concatenation with a struct, or calling an interface method on a struct without generics.
- Generic collections (List<int>) avoid boxing entirely.
- Interpolated string handlers / Span can avoid hidden allocations.
What problem do Span<T> and Memory<T> solve?
- Span<T> is a stack-only, allocation-free view over contiguous memory (arrays, strings, stackalloc, native buffers).
- It enables slicing and zero-copy parsing without creating substrings or arrays.
- It cannot be used across await or stored on the heap; Memory<T> is the heap-friendly counterpart.
- Ideal for high-performance string/serialization/numeric code.
- It has lifetime rules enforced by the compiler (no capturing in async or lambdas).
How do you release resources asynchronously?
- IAsyncDisposable provides DisposeAsync for resources that need async cleanup (e.g. DbConnection, streams).
- `await using` (C# 8) disposes asynchronously at scope exit.
- Implement the Dispose pattern for sync plus DisposeAsync where both are needed.
- Do not block on async cleanup with .Result — it can deadlock.
- ValueTask is often returned by DisposeAsync to avoid allocation in the common synchronous path.
What are C# records?
- A record is a reference type with value-based equality and a concise positional syntax.
- The compiler generates a constructor, init-only properties, Equals, GetHashCode, and ToString.
- with-expressions create a copy with some properties changed (non-destructive mutation).
- record struct gives the same features as a value type.
- Use records for immutable data/DTOs and value semantics.
How does C# pattern matching work?
- is patterns type-test and bind: if (o is Point { X: 0 } p).
- switch expressions with patterns and when guards replace long if/else chains.
- Property and positional patterns destructure objects and records.
- List patterns (C# 11+) match sequences.
- Exhaustiveness is encouraged with a discard arm.
What are async streams (IAsyncEnumerable)?
- An async iterator uses async IAsyncEnumerable<T> with yield return and awaits.
- Consume with await foreach, optionally with WithCancellation.
- They stream data as it becomes available without buffering everything.
- EnumerateAsync needs [EnumeratorCancellation] to flow the token.
- Great for paged APIs, file reads, and realtime data.
Compare lock, Monitor, and Interlocked.
- lock(obj) is sugar for Monitor.Enter/Exit with a try/finally.
- Monitor adds TryEnter with timeout, Wait/Pulse for signaling.
- Interlocked provides lock-free atomic operations (Increment, CompareExchange).
- Never lock on `this`, strings, or a Type — external code can deadlock you.
- Prefer System.Threading.Lock (modern) or dedicated lock objects.
What are the DI lifetimes and their pitfalls?
- Singleton: one instance for the app; scoped: one per scope/request; transient: new each resolve.
- Captive dependency: injecting a scoped service into a singleton pins it forever.
- Resolve scoped services per request via the request scope, not the root provider.
- IDisposable services are disposed when their scope ends.
- Constructor injection is preferred; avoid the service-locator anti-pattern.
What are expression trees?
- A lambda assigned to Expression<Func<...>> becomes a data structure describing the code.
- IQueryable providers (EF Core) translate the tree into SQL instead of executing it in memory.
- You can walk or rewrite the tree (e.g. add filters, build dynamic predicates).
- Not every C# expression can be translated, which causes runtime translation errors.
- Compile() turns the tree back into an executable delegate.
What is an indexer?
- An indexer (this[int i]) lets an object be indexed like an array.
- It can have get/set (and init) and multiple parameters (this[int x, int y]).
- Indexers are properties in disguise and don't require a backing collection.
- They enable dictionary-like syntax on custom types.
- Range/Index (C# 8) can be accepted for slicing-like access.
What are covariance and contravariance in generics?
- out T makes a generic covariant: IEnumerable<string> is assignable to IEnumerable<object>.
- in T makes it contravariant: IComparer<object> to IComparer<string>.
- Variance applies to interface and delegate type parameters, not classes.
- It is safe only for read-only (out) or write-only (in) positions.
- Arrays are covariant at runtime, which is why ArrayTypeMismatchException exists.
How do Tasks relate to the thread pool?
- Task.Run queues work to the thread pool for CPU-bound work; async I/O doesn't consume a thread while waiting.
- The thread pool grows under load and has hysteresis; blocking many threads starves it.
- LongRunning hints a dedicated thread.
- Never block on .Result/.Wait() in async code — that can deadlock and exhaust the pool.
- ValueTask avoids allocation for frequently synchronous completions.
What is reflection used for and what does it cost?
- Reflection inspects and invokes members at runtime (typeof, GetType, Activator, MethodInfo.Invoke).
- It powers serializers, ORMs, DI, and attribute-driven frameworks.
- Costs: slower calls, no compile-time checking, trimming/AOT issues.
- Cache MethodInfo/delegates and use source generators or compiled expressions for hot paths.
- Attributes are the common metadata read via reflection.
Common mistakes
- Blocking on Task.Result or .Wait() inside async code and dead-locking the captured context.
- Assuming async void is fine outside event handlers, or that await always starts a new thread.
- Forgetting to dispose IDisposable objects, or thinking the GC also releases unmanaged handles.
- Believing struct is always faster, while ignoring boxing and defensive copies.
How to rehearse these answers
- Explain what the compiler generates for an async method and where the continuation resumes.
- Compare IEnumerable and IQueryable, then show a query that accidentally executes more than once.
- Write an IDisposable with a finalizer using the dispose pattern and explain GC.SuppressFinalize.
- Demonstrate boxing for a struct used as object or stored in a non-generic collection.
New to technical interviews? Read how to rehearse coding and system design.
Other languages
JavaScript interview questions · Python interview questions · Java interview questions · C++ interview questions · Go interview questions