View as Markdown
Interviews

C# Interview Questions for Experienced Developers: What Panels Are Really Testing

CareerCTO14 min read

C# Interview Questions for Experienced Developers: What Panels Are Really Testing — Interviews

Senior C# interviews rarely reward memorized definitions. This guide groups the common questions by the underlying signal an interviewer is checking for and shows the follow-up that separates a rehearsed answer from real understanding.

Most C# interview prep is a list of questions with a paragraph of answer under each one. That works for a junior screen. It falls apart the moment a panel interviewing someone with five or more years of experience asks a follow-up you did not rehearse.

Senior C# interviews are not testing whether you can define "boxing" or recite the difference between IEnumerable and IQueryable. They are testing whether you understand the mechanism well enough to predict what happens in a case you have not seen before. The definition is the entry ticket. The follow-up is the actual test.

This guide groups the questions you will meet by the signal behind them, not by topic name. For each group we give the opening question, the follow-up that exposes a shaky answer, and what a candidate who has actually debugged this in production tends to say differently.

If you are also prepping the web layer, the ASP.NET Core interview questions guide covers the same style of drill for the pipeline and hosting model.

Why the same question can pass or fail a candidate

Interviewers reuse the same handful of questions across candidates because the questions are good at separating two groups: people who read the answer once and people who have hit the behavior directly. The wording barely changes. What changes is the follow-up.

A memorized answer describes what the documentation says. A real answer describes what happens next, because the candidate has watched it happen - in a debugger, in a production incident, or in code review. That is the gap this guide is built to close.

Signal one: do you understand the CLR memory model, not just "stack vs heap"

The stock question is "what is the difference between value types and reference types?" Almost every candidate gets this right at the surface level. The follow-up decides whether they understand it.

The question that actually separates candidates

"If I put a struct inside a List<T> and mutate a field through a foreach loop, what happens?" A candidate who only memorized "structs are value types" will often guess wrong here, because the mutation silently fails or does not compile, depending on how the loop variable is used.

struct Point { public int X; }

var points = new List<Point> { new Point { X = 1 } };

foreach (var p in points)
{
    p.X = 99; // compiles, but is a no-op on a copy
}

The loop variable p is a copy of the struct pulled out of the list. Setting p.X changes the copy, not the element inside points. A candidate who understands this will say so unprompted and mention that foreach over a List<T> of structs is a common source of "my change did not take" bugs.

Why interviewers care

This is not trivia. It is a real failure mode in codebases that mix structs and collections for performance reasons. A candidate who has hit it once remembers it for good. One who has not will guess "it updates the list" because that is the intuitive but wrong answer.

"Why does the CLR have generations at all?" The weak answer is "to make GC faster." The stronger answer explains the actual assumption: most objects die young, so the collector only sweeps generation 0 most of the time and promotes long-lived survivors upward, which avoids repeatedly scanning objects that are still alive.

The good follow-up here is "what kind of allocation pattern defeats this assumption?" Large temporary objects and objects that get promoted to generation 2 and then die anyway (the "mid-life crisis" pattern) are the answer a senior candidate should reach for.

Boxing, and why it still matters with generics

"Does calling a generic method with a struct type argument still cause boxing?" is a good filter, because the honest answer is "it depends." Generic code compiled for a value-type argument is specialized by the JIT and avoids boxing. The moment that struct is passed through a non-generic API expecting object, or stored in a non-generic collection, boxing happens again.

A candidate who has profiled a hot path for allocations will usually mention Span<T> or generic constraints as the fix, because both let you keep value types on the stack instead of promoting them to the heap unnecessarily.

Line drawing of a magnifying glass hovering over a tangled memory diagram with boxes and arrows

Signal two: do you understand async/await as control flow, not as a keyword pair

"What does async and await do?" is asked in almost every senior .NET interview. Nearly everyone can say "it lets you write asynchronous code that looks synchronous." That sentence proves nothing.

The follow-up that matters

"What actually happens to the calling thread when you hit an await?" The correct answer: the method returns control to its caller immediately, and the rest of the method runs as a continuation, scheduled by the awaiter, when the awaited task completes. No thread is blocked while the awaited operation is in flight.

A common wrong mental model is that await "pauses" the thread the way Thread.Sleep would. Candidates who hold this model tend to also misuse .Result and .Wait() to "make async code synchronous," which is exactly the pattern that causes deadlocks in UI and older ASP.NET applications.

The deadlock question

"Why does calling .Result on a task from a UI thread sometimes deadlock, but the same code works fine in a console app?" This question is a strong filter because it requires understanding SynchronizationContext.

Environment Has a SynchronizationContext? Deadlock risk with .Result
WPF / WinForms UI thread Yes, captures back to the UI thread High
Classic ASP.NET request Yes, captures back to the request context High
Console app No Low
ASP.NET Core No, by default Low

The mechanism: await captures the current context by default and resumes the continuation on it. If the UI thread is blocked waiting on .Result, and the continuation needs that same thread to run, neither side can move. The practical fix candidates should mention is ConfigureAwait(false) in library code that does not need to resume on the original context.

The question about exceptions

"If an async void method throws, where does the exception go?" This one catches out even experienced developers who have only ever used async Task.

An exception from async void cannot be awaited or caught by the caller; it is raised on the SynchronizationContext that was active when the method started, which usually means it crashes the process or gets swallowed depending on the host.

The follow-up worth having ready: "so when is async void actually correct?" Answer: event handlers, because their signature is fixed by the framework, and nowhere else.

Signal three: do you understand LINQ's deferred execution, not just its syntax

Every candidate can write a Where().Select() chain. Far fewer can explain when it actually runs.

The core question

"When does this line actually query the database?"

var query = dbContext.Users.Where(u => u.IsActive);

The correct answer is: not yet. query is an expression tree describing the work, not a result. The database is hit only when the query is enumerated - by a foreach, a .ToList(), a .Count(), or similar. This is deferred execution, and it is one of the most-cited sources of subtle bugs and performance problems in EF Core code.

The follow-up that separates real understanding

"If I call .Where() twice on the same IQueryable in two different methods, does the database get queried twice?" A candidate who understands deferred execution says yes, once per enumeration, unless something materializes the result in between with .ToList() or similar.

This matters because a common bug pattern is passing an IQueryable around and accidentally running the same query multiple times, or worse, running it inside a loop.

The N+1 question

"What is the N+1 query problem, and how would you spot it in a code review?" This question tests whether the candidate has actually looked at generated SQL, not just heard the term. A good answer describes lazy-loaded navigation properties accessed inside a loop, each triggering its own round trip, and names .Include() or explicit projection as the fix.

// N+1: one query per order to fetch its customer
foreach (var order in orders)
{
    Console.WriteLine(order.Customer.Name);
}

// Fixed: one query, eager-loaded
var orders = dbContext.Orders.Include(o => o.Customer).ToList();

Candidates who have only used LINQ over in-memory collections sometimes answer this question as if it were about CPU cost, not I/O round trips. That mix-up is itself a useful signal for the interviewer.

The IEnumerable vs IQueryable question

"You have an IQueryable<User> and you call .Where(u => IsSenior(u)) where IsSenior is a regular C# method, not an expression. What happens?" The honest answer is that this throws at runtime, because IsSenior cannot be translated into SQL - the provider needs an expression tree, not a compiled method reference, to build the query.

The fix a senior candidate reaches for is either rewriting the condition as an inline expression the provider can translate, or calling .AsEnumerable() first to force the rest of the chain to run in memory against already loaded objects.

Knowing which of those two options is appropriate, and why switching to .AsEnumerable() too early can silently pull an entire table into memory, is the real test behind this question.

Signal four: do you understand DI lifetimes, not just the three words

"What is the difference between transient, scoped, and singleton?" is close to universal in senior .NET interviews. The three definitions are easy to memorize. The bug they cause is not.

Lifetime New instance per Typical use
Transient Every resolution Lightweight, stateless services
Scoped Per request (in ASP.NET Core) Services that use a DbContext
Singleton Once for the app lifetime Configuration, caches, stateless clients

The question that actually matters

"What happens if a singleton service takes a scoped service as a constructor dependency?" The correct answer is that the default container throws at resolution time, because the singleton would otherwise hold on to the first scope's instance forever, which is exactly the captive dependency problem the validation is designed to catch.

A candidate who has not hit this describes the lifetimes correctly but has no answer for what breaks when you mix them. A candidate who has hit it describes the captive dependency by name and usually mentions the specific symptom: a DbContext being reused across requests, causing stale data or threading exceptions, because it got trapped inside a singleton.

The disposal question

"Who disposes a scoped service, and when?" The container does, at the end of the scope - the HTTP request, in a typical ASP.NET Core app. This is worth knowing because it explains why manually caching a scoped service in a static field is dangerous: you end up holding a reference to something the container has already disposed.

The factory pattern question

"How do you get a scoped or transient service from inside a singleton safely?" The correct pattern is injecting IServiceScopeFactory into the singleton and creating a new scope each time you need the dependency, rather than injecting the scoped service directly. This gives the singleton a clean way to reach into a short-lived dependency without becoming a captive itself.

A candidate who reaches for this pattern unprompted, instead of just saying "you can't," shows they have actually had to solve this in a background service or a hosted worker, both common places where a singleton legitimately needs scoped data like a DbContext.

Signal five: do you understand equality and comparison, not just override syntax

"How do you override equality for a class?" gets a mechanical answer: override Equals, override GetHashCode, maybe implement IEquatable<T>. The follow-up shows whether the candidate understands the contract, not just the syntax.

The contract question

"What breaks if GetHashCode is not consistent with Equals?" The specific failure: objects that are Equals but have different hash codes can end up in different buckets in a Dictionary or HashSet, so lookups silently fail to find an entry that is logically present.

This is a bug that is very hard to spot in a debugger because everything looks correct until you query the collection.

The records follow-up

"If C# records already generate value-based equality, why would you ever still write a custom Equals?" A strong answer: when equality needs to ignore certain fields (an audit timestamp, a cache key) or when the type has reference-type fields, like collections, where you want deep comparison instead of the default reference comparison records give you for those members.

Signal six: do you understand exceptions as control flow, not just try/catch syntax

"What is the difference between throw ex; and throw; inside a catch block?" is a small question with a large gap between the memorized and real answer. Both look identical when you run the code once and it works.

Why the difference matters

throw; re-throws the current exception and preserves its original stack trace, including every frame from where it actually happened. throw ex; resets the stack trace to the current catch block, which throws away the information you need to find the real source of the bug in production logs.

try
{
    DoWork();
}
catch (InvalidOperationException ex)
{
    LogAndCleanup(ex);
    throw; // preserves the original stack trace
    // throw ex; would reset it to this line
}

A candidate who has debugged a production incident using a stack trace that turned out to be useless will bring this up without being asked, because it cost them real time once.

The follow-up on exception filters

"When would you use catch (Exception ex) when (condition) instead of an if check inside the catch block?" The strong answer: an exception filter runs before the stack unwinds, so it does not disturb the original call stack if the condition is false and the exception needs to propagate further up.

An if check inside a plain catch has already unwound the stack by the time you decide to re-throw, which is worse for diagnostics in exactly the cases where you need them most.

Line drawing of a stack trace unwinding through layered function call boxes toward an error icon

How to actually prepare, not just read

Reading this guide once will not make these answers stick. The candidates who do well are the ones who have caused, or debugged, at least one of these behaviors directly. If you have not, the fastest way to build a real answer is to write the ten-line reproduction yourself and watch it fail before you read the explanation.

It also helps to know what the interview process on the other side actually looks like. Our guide on how technical interview processes are designed explains why panels structure rounds the way they do, and why the same question often shows up twice with different framing.

Where this fits in a broader .NET interview

C# fundamentals are usually one round out of several. A typical senior .NET loop also covers the web framework layer and the database layer, and skipping either leaves a gap a panel will find. The .NET full stack developer roadmap lays out what a complete stack looks like if you want to check your prep against the whole picture, not just the language.

Database questions come up constantly in these same loops, usually right after the LINQ and EF Core section, because the two are hard to separate in practice. If SQL is the weaker side of your prep, the SQL Server interview questions guide uses the same grouped-by-signal approach for query plans, indexing, and locking.

What a strong answer sounds like, structurally

Across every group above, the pattern in a strong answer is the same. It names the mechanism first, then the specific failure mode it causes, then a concrete example the candidate has actually seen. A weak answer stops after the definition and waits for the next question.

You do not need to have caused every failure mode above yourself. You do need an honest answer when you have not - "I have not hit that directly, but based on how the scheduler works, I would expect..." is a far better answer than a confident guess, and most senior interviewers rate it accordingly.

Line drawing of two developers at a table, one holding note cards and one holding a laptop mid-debug

Where CareerCTO fits into this

CareerCTO does not run these interviews for employers, and a verified badge on a profile does not mean someone has passed a C# interview - it means Questpond's records confirm the person completed a specific cohort on a specific date.

What it does give you is a shortcut past the resume-screening stage: employers using the directory already trust that baseline, so the conversation with them starts closer to a technical round like the one this guide covers.

If you want to see what employers are actually asking for at the senior C# level right now, browsing reviewed job openings is a faster signal than any list of interview questions, because the descriptions tell you which of these five areas a given team cares about most.

What to do next

Pick the one group above where your answer was closest to the "weak" version, not the strongest one. Write the reproduction code yourself, run it, and write down what actually happened before you move to the next group. That is a better use of an evening than reading twenty more questions you will not remember under pressure.

When you are ready to put this prep to use, browse verified developer profiles to see how other .NET developers present this kind of depth on their own profile, or build your profile so a completed cohort and a clear project history are the first thing an employer sees before the interview even starts.

Keep reading