Skip to main content
C# advanced Lesson 25 of 25

C# Interview Preparation

Top 35 C# interview questions and answers covering language features, OOP, async, and .NET internals.

Core Language

These questions test your understanding of C#‘s type system and memory model. They come up in almost every interview because they reveal whether you understand what the runtime is actually doing, not just the syntax.

Q1: What is the difference between value types and reference types?

Value types (structs, enums, primitives) store their data directly where the variable lives — on the stack for locals, inline in the containing object on the heap. Assignment copies the value. Reference types (classes, interfaces, arrays) store a reference (pointer) to heap-allocated data. Assignment copies the reference; both variables point to the same object.

int a = 5; int b = a; b = 10;
Console.WriteLine(a);  // 5 — assignment made a copy, a is unchanged

var list1 = new List<int>(); var list2 = list1;
list2.Add(1);
Console.WriteLine(list1.Count);  // 1 — both variables point to the same list

Q2: What is boxing and unboxing?

Boxing wraps a value type in a heap-allocated object. Unboxing extracts it back. Both incur a heap allocation and a copy, causing GC pressure in hot paths. The fix is generics — List<int> stores ints directly without boxing, while ArrayList boxes every int.

int n = 42;
object boxed   = n;           // boxing — heap allocation + copy
int unboxed    = (int)boxed;  // unboxing — type check + copy

// Avoid boxing by using generics instead of object
List<int> good = new();       // no boxing — stores ints directly
ArrayList bad  = new();       bad.Add(42);  // boxes every int

Q3: What is the difference between const and readonly?

const is a compile-time constant baked into the IL — only primitives and strings are allowed. readonly is set at runtime (at declaration or in the constructor) and can hold any type. const is implicitly static; readonly can be instance or static. If you change a const in a library, all assemblies that reference it must be recompiled; readonly does not have this problem.


Q4: What is the difference between string and StringBuilder?

string is immutable — every operation creates a new string on the heap. StringBuilder is a mutable buffer that appends to an internal array and only allocates the final string at the end. For building strings in a loop, StringBuilder is O(N) while + concatenation in a loop is O(N²) due to repeated allocations and copies.


Q5: Explain nullable reference types.

With <Nullable>enable</Nullable> in the project file, the compiler tracks whether reference types can be null. string means non-null; string? means nullable. The compiler emits warnings when a nullable value is dereferenced without a null check. This shifts null reference bugs from runtime crashes to compile-time warnings. The null-forgiving operator ! suppresses a warning when you know the value cannot be null despite what the compiler thinks.


Object-Oriented Programming

OOP questions test whether you understand the principles behind C#‘s class system — not just syntax, but when to use each feature and why.

Q6: What is the difference between abstract class and interface?

An abstract class can have state, constructors, concrete methods, and protected members. A class can only inherit one abstract class. An interface (pre-C# 8) defines a contract with no implementation. A class can implement many interfaces. Use an abstract class when you have shared implementation in a type hierarchy; use an interface to define a contract that unrelated types can fulfill.


Q7: What is the difference between override and new on a method?

override replaces the virtual method in the polymorphic dispatch table — a base-typed reference calling the method will invoke the derived version. new hides the base method at compile time without replacing it in the dispatch table — a base-typed reference still calls the base version. Almost always use override; new leads to confusing behavior.

class Base    { public virtual void Foo() => Console.WriteLine("Base"); }
class Derived : Base { public override void Foo() => Console.WriteLine("Derived"); }

Base b = new Derived();
b.Foo();  // "Derived" — override participates in polymorphic dispatch

Q8: What does sealed do?

On a class, sealed prevents inheritance. On a virtual method, it prevents further overriding in subclasses. The JIT compiler can de-virtualize calls to sealed methods — it knows there is no subclass to dispatch to, so it can inline the call and eliminate the virtual dispatch overhead entirely.


Q9: Explain the Liskov Substitution Principle.

A subtype must be substitutable for its base type without altering the correctness of the program. In practice: a derived class should honor the contracts of its base class — it should not strengthen preconditions (demand more from callers), weaken postconditions (deliver less), or throw exceptions the base contract does not allow. Violating LSP means your inheritance hierarchy is modeling something other than an “is-a” relationship.


Q10: What is the difference between method overloading and method overriding?

Overloading defines multiple methods with the same name but different parameter signatures in the same class — the compiler resolves which one to call at compile time based on argument types. Overriding provides a new implementation for a virtual method declared in a base class — the runtime resolves which one to call at runtime based on the object’s actual type (polymorphism).


Generics and Collections

Q11: Why are generics better than object?

Generics provide compile-time type safety without casting, which catches type errors at compile time rather than runtime. For value types, they also eliminate boxing — List<int> stores ints directly in a contiguous array, while ArrayList boxes every int into a separate heap-allocated object, producing GC pressure and slower access.


Q12: What are generic constraints?

Constraints restrict which types are valid for a type parameter, enabling you to call methods or use operators on the type parameter. Common ones:

  • where T : class — reference types only
  • where T : struct — value types only
  • where T : new() — must have a parameterless constructor (enables new T())
  • where T : IComparable<T> — must implement the interface (enables .CompareTo)

Q13: What is covariance and contravariance?

Covariance (out T) allows a more-derived type to be used where a base type is expected — IEnumerable<Dog> can be assigned to IEnumerable<Animal> because you only read from it. Contravariance (in T) is the reverse — Action<Animal> can be assigned to Action<Dog> because you only write to it. Both only apply to interfaces and delegates, and only to reference types.


Q14: When would you use Dictionary vs List for lookup?

Dictionary<K,V> has O(1) average lookup by key using a hash table. List<T>.Contains and List<T>.Find are O(N) linear scans. Use Dictionary when you need to look up elements by a key repeatedly. Use List when you need ordered, index-based access, or when the collection is small enough that linear scan is acceptable.


LINQ and Functional Patterns

Q15: What is deferred execution in LINQ?

Most LINQ operators (Where, Select, OrderBy) do not execute when called — they build a query description (a chain of delegates). Execution happens when you enumerate the result with foreach, ToList, Count, First, etc. This means the query reflects the current state of the data at the time of iteration, not when it was defined. It also means a query over an IQueryable<T> (like EF Core) is not sent to the database until you materialize it.


Q16: What is the difference between IEnumerable<T> and IQueryable<T>?

IEnumerable<T> processes data in memory using delegates — it pulls all data from the source first and filters/projects in C#. IQueryable<T> holds an expression tree that a query provider (like EF Core) translates into a server-side query (SQL). Always use IQueryable<T> with EF Core so filters, projections, and ordering are applied at the database, not after loading all rows into memory.


Async and Concurrency

Q17: How does async/await work internally?

The C# compiler transforms an async method into a state machine struct. Each await becomes a suspension point with a state number. When the awaited operation completes, the runtime calls MoveNext() on the state machine, which resumes execution from the correct point — potentially on a different thread depending on the SynchronizationContext. No thread is blocked while I/O is in flight.


Q18: What is the difference between Task.Run and async/await?

Task.Run offloads CPU-bound work to a thread pool thread — use it when you have computation that would block the calling thread. async/await is for I/O-bound work — it releases the current thread while waiting for a network response, file read, or database query. Using Task.Run for I/O wastes a thread; using async/await for pure CPU computation just adds state machine overhead without freeing anything.


Q19: What causes a deadlock with async code?

Calling .Result or .Wait() on a Task in an environment that has a SynchronizationContext (WinForms, ASP.NET classic) causes a deadlock: the calling thread holds the context and blocks waiting for the Task to finish; the Task’s continuation needs the same context to resume but cannot acquire it because the calling thread is blocked. The fix is to use await all the way up the call chain.

// Deadlock in ASP.NET classic — the request thread blocks waiting for the continuation
string data = GetDataAsync().Result;   // BLOCKS, and then deadlocks

// Fix — await all the way; never block on async code
string data = await GetDataAsync();

Q20: What is ConfigureAwait(false)?

It tells the awaiter not to capture the current SynchronizationContext when scheduling the continuation. In library code, this prevents deadlocks when callers block on async code (as above) and avoids a small overhead from context marshaling. In ASP.NET Core, there is no SynchronizationContext, so it makes no functional difference — but it remains good practice in libraries to be safe when consumed from other frameworks.


Q21: What is CancellationToken used for?

It enables cooperative cancellation of async operations. The caller creates a CancellationTokenSource, passes the Token to async methods, and calls Cancel() to signal that the result is no longer needed. The operation checks token.IsCancellationRequested or calls token.ThrowIfCancellationRequested() at safe points. Well-designed async APIs accept a CancellationToken parameter (defaulting to default) so callers can always opt in to cancellation.


Memory and Performance

Q22: How does the .NET garbage collector work?

The GC is a generational, tracing collector. New objects start in Gen 0. Objects that survive a Gen 0 collection are promoted to Gen 1, and from there to Gen 2. Gen 0 is collected frequently and quickly; Gen 2 is collected rarely but is expensive. Large objects (>85KB) go directly to the Large Object Heap. The GC uses mark-and-sweep: it traces all reachable objects from GC roots (stack variables, statics, handles), marks them as live, then reclaims the rest.


Q23: What is IDisposable and when should I implement it?

IDisposable.Dispose() is the standard pattern for deterministic cleanup of resources that should not wait for the GC — file handles, database connections, network sockets, and any object that holds another IDisposable. Implement it when your class owns a resource that needs to be released promptly. Always call Dispose via using so it runs even if an exception is thrown. For classes with unmanaged resources, also implement a finalizer as a safety net.


Q24: What is Span<T> and when should I use it?

Span<T> is a stack-only ref struct that represents a contiguous slice of memory — a string, an array, or a stack-allocated buffer — without allocating. Use it to slice strings or arrays without copying, to parse data in tight loops, or to work with stack-allocated buffers. Its stack-only restriction means it cannot be stored in a field or used across await boundaries — use Memory<T> for those cases.


Design and Patterns

Q25: What is dependency injection and why use it?

Dependency injection is a pattern where a class receives its dependencies from an external source (constructor parameters) rather than creating them itself. This makes classes loosely coupled — they depend on abstractions (interfaces), not concrete implementations. The benefits are testability (swap real implementations for fakes in tests), configurability (change implementations in one place), and lifetime management (the DI container controls when objects are created and disposed). The .NET DI container in Microsoft.Extensions.DependencyInjection resolves and injects dependencies automatically.


Q26: What is the Repository pattern?

Repository abstracts the data access layer behind an interface so the rest of the application never depends on EF Core, Dapper, or any specific database technology. A service depends on IOrderRepository; the concrete implementation (EfOrderRepository) lives in an infrastructure layer. This makes business logic independently testable with an in-memory fake, and makes it possible to swap the database without changing any service code.


Q27: What is the difference between eager loading, lazy loading, and explicit loading in EF Core?

  • Eager loading (Include): loads related data in a single query using a SQL JOIN — the safest default.
  • Lazy loading: loads related data automatically on first property access, using proxies. Convenient but dangerous in loops — each access fires a separate query, causing the N+1 problem.
  • Explicit loading: you manually call Entry(entity).Collection(...).LoadAsync() to load specific navigation properties when you decide you need them.

Prefer eager loading for known navigation requirements and avoid lazy loading in APIs or any code path that iterates collections.


C# Language Features

Q28: What are records?

Records (C# 9) are compiler-generated classes or structs with value-based equality, a generated ToString, non-destructive mutation via with expressions, and deconstruction. Two records with the same property values are equal regardless of reference. They are ideal for DTOs, command/query objects, domain events, and any immutable data structure where equality should be based on content rather than identity.


Q29: What are expression-bodied members?

Syntactic shorthand using => for members whose body is a single expression: methods, properties, constructors, finalizers, and operators. public int Square(int n) => n * n; compiles to exactly the same IL as the block-bodied version. They improve readability for simple members but can obscure intent in complex ones — use them when the expression is self-explanatory.


Q30: What is pattern matching?

A set of C# language features for testing values and destructuring them in a single expression. Key patterns include: type patterns (is string s), relational patterns (> 0), property patterns ({ Name: "Alice" }), positional patterns for records and tuples, list patterns ([1, 2, ..]), and switch expressions. They replace cascading null checks, if/else if type chains, and manual property extraction with concise, readable code.


Q31: What are local functions?

Methods defined inside another method, visible only within the enclosing scope. They can close over the outer method’s variables and are useful for recursive helpers (where a lambda cannot be directly recursive) or for naming a multi-step operation that only makes sense within one method. Mark them static when they don’t need to capture outer variables — this prevents accidental captures and can enable inlining.


Q32: What is the difference between Task<T> and ValueTask<T>?

Task<T> always allocates a new object on the heap, even when the result is already available synchronously. ValueTask<T> is a struct that avoids this allocation when the result is available synchronously — it stores either the result value directly or a reference to the underlying Task<T>. Use ValueTask<T> in hot-path code where the synchronous path is common (cache lookups, already-completed operations). Do not await a ValueTask<T> more than once.


Q33: What are source generators?

Source generators are Roslyn-based compile-time tools that inspect your code’s syntax and semantic model and emit additional C# source files as part of compilation. They enable zero-overhead, reflection-free implementations of features that would otherwise use runtime reflection: System.Text.Json source generation for serialization, [GeneratedRegex] for compiled regexes, and incremental DI registration. They produce code that is compiled directly into your assembly.


Q34: What is the difference between == and Equals?

For value types, both compare content by default. For reference types, == compares identity (same reference) unless overloaded; Equals can be overridden to compare content — which string and record both do. When you override Equals, you must also override GetHashCode to maintain the contract that equal objects have equal hash codes. Consider also overloading == for value-like types.


Q35: How would you implement a thread-safe counter?

// Option 1: Interlocked — fastest, lock-free, for simple atomic operations
private int _count = 0;
public int Increment() => Interlocked.Increment(ref _count);
public int Get() => Volatile.Read(ref _count);  // ensures visibility across threads

// Option 2: lock — for complex operations that need to be atomic together
private readonly object _lock = new();
private int _count;
public int Increment() { lock (_lock) return ++_count; }

// Option 3: Lazy<T> — for a value that is computed exactly once, thread-safely
private static readonly Lazy<MyService> _instance =
    new(() => new MyService(), LazyThreadSafetyMode.ExecutionAndPublication);
public static MyService Instance => _instance.Value;

Frequently Asked Questions

What topics are most commonly asked in C# interviews?
Expect questions on value vs reference types, boxing/unboxing, async/await, LINQ, generics, interfaces vs abstract classes, memory management (GC, Dispose pattern), and OOP principles. Senior roles also ask about performance optimization and design patterns.
How deep should I know .NET internals for an interview?
Most roles expect a solid understanding of the CLR, GC generations, and JIT compilation at a conceptual level — not at the implementation level. Know enough to explain why something behaves a certain way and what the performance implications are.