DEV Community

kirandeepjassal-crypto
kirandeepjassal-crypto

Posted on Originally published at prepstack.co.in

C# Interview Questions for Senior Developers in 2026 — async/await, GC & Memory, the Type System, and LINQ (Deep Answers)

Junior C# interviews test definitions — "what is a delegate," "value vs reference type." Senior interviews test judgment: not what async is, but why your .Result deadlocked in production; not what a struct is, but when copying one silently doubled your allocations.

This is 30 questions across the four areas seniors actually get grilled on — async & concurrency, memory & GC, the type system, and LINQ & production patterns. Every answer follows the same shape: a short answer you could give in one breath, a deep dive into the mechanism (with code), and the senior signal the interviewer is listening for. Every snippet is from Mattrx — a multi-tenant marketing-analytics SaaS on .NET 9 / ASP.NET Core / Azure SQL / Redis, with a 1.2B-row events table and a CQRS core.

The lens for all 30

A junior answers what; a senior answers why, when-not-to, and what it costs. "How does async work?" gets "it waits for the task" from a junior and "it suspends, frees the thread, and the continuation is scheduled — and here's how blocking on it deadlocks" from a senior.

Block A highlight — what actually happens when you await

await is not "wait here." It's "suspend here, free the thread, and schedule the rest as a continuation." That's why an ASP.NET Core server handles thousands of concurrent requests on a small thread pool — threads aren't parked waiting on I/O.

await _repository.AggregateAsync(...)
      │  suspend — thread returned to the pool (NOT blocked)
      ▼
   ...Azure SQL runs the aggregate...
      │  completes
      ▼
continuation resumes on a pooled thread
Enter fullscreen mode Exit fullscreen mode

And the classic deadlock: in a context-capturing host (classic ASP.NET, WPF), blocking the one context thread with .Result while the continuation needs that same thread to resume is a self-deadlock. The fix is "async all the way," never block.

// DEADLOCK RISK: blocks the context thread
public CampaignKpis GetKpis(...) => _service.GetKpisAsync(...).Result;
// CORRECT — async all the way:
public Task<CampaignKpis> GetKpisAsync(..., CancellationToken ct) => _service.GetKpisAsync(..., ct);
Enter fullscreen mode Exit fullscreen mode

Bounded concurrency is the other outage-preventer — never unbounded Task.WhenAll over 10,000 tasks:

await Parallel.ForEachAsync(tenants,
    new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = ct },
    async (tenant, token) => await GenerateNightlyReportAsync(tenant, token));
Enter fullscreen mode Exit fullscreen mode

Block B highlight — GC, allocations, and the HttpClient trap

.NET's GC is generational: most objects die young and are collected cheaply in gen 0; survivors get promoted to gen 1, then gen 2 (whole-heap, expensive); objects >85KB go on the LOH. The win on Mattrx — 2.1GB → 380MB working set — was mostly about not promoting short-lived allocations to gen 2.

And the senior HttpClient gotcha — know both failure modes:

// BUG: socket exhaustion under load (TIME_WAIT)
using var http = new HttpClient();
// A single static HttpClient fixes that but never picks up DNS changes.
// CORRECT: IHttpClientFactory — pooled, DNS-aware handlers.
factory.CreateClient("stripe").PostAsync(...);
Enter fullscreen mode Exit fullscreen mode

Boxing is the silent allocator — an enum boxed to object per event is millions of gen-0 objects/sec at ingestion rate. Span<T>/Memory<T> give zero-allocation slicing (but Span<T> can't cross an await — it's a ref struct).

Block C highlight — deferred execution

A LINQ query over IEnumerable<T> doesn't run until you enumerate it; enumerating it twice runs it twice:

// BUG: query executed TWICE — Count() enumerates, then the foreach enumerates again.
IEnumerable<CampaignEvent> q = repo.Query(tenant);
if (q.Count() > 0) foreach (var e in q) Handle(e);   // two DB round-trips
// FIX: materialize once.
var events = repo.Query(tenant).ToList();
Enter fullscreen mode Exit fullscreen mode

Block D highlight — the EF Core IQueryable trap

IQueryable<T> builds an expression tree EF translates to SQL (filtering runs in the database); IEnumerable<T> runs LINQ in memory. Accidentally crossing the boundary is the difference between a WHERE clause and loading 1.2B rows:

// GOOD: filter runs in Azure SQL
var q = _db.CampaignEvents.Where(e => e.TenantId == tenant && e.OccurredAt >= from);
// TRAP: ToList() first => the Where now runs in memory over the ENTIRE table.
var bad = _db.CampaignEvents.ToList().Where(e => e.TenantId == tenant);
Enter fullscreen mode Exit fullscreen mode

Plus the captive-dependency bug (a singleton capturing a scoped DbContext), ConcurrentDictionary.GetOrAdd running its factory twice under load (fix: Lazy<T>), and why returning IEnumerable<T> from a repository hands the caller control of when and how many times your query executes — after the DbContext is disposed, or twice.

The through-line

Senior C# is a handful of production truths: async suspends, it doesn't block; allocations become GC pressure; the type system is a compile-time aid, not a runtime guarantee; and the boundary between in-memory and in-database (or one thread and many) is where systems break. Answer every question by naming the mechanism, the real failure it prevents, and the trade-off.

The full guide has all 30 — each with the short answer, the deep dive with code, and the exact senior signal:

https://prepstack.co.in/blog/csharp-senior-interview-questions-async-memory-linq-concurrency-deep-answers

Originally published on PrepStack.

Top comments (0)