.NET MASTER
Home

Advanced .NET Internals

Deep dive into how the CLR works, with code examples and interview traps.

1. Memory Management & GC

The Garbage Collector (GC) doesn't just "free memory". It manages it in Generations to optimize performance.

Gen 0

Short-lived objects. Collected very fast. Most objects die here.

Gen 1 / Gen 2

Survivors get promoted. Gen 2 collections are rarer but expensive.

LOH

Large Object Heap (>85KB). Historically not compacted → fragmentation risk.

💡 Interview Trap: LOH ≠ always “never compacted”

In modern .NET, LOH compaction can be enabled (on demand). Still, allocating many big arrays/strings may fragment memory. The main point: avoid frequent large allocations and reuse buffers when possible.

Optimization Trap: The String allocation

// ❌ BAD: Creates many objects in Gen 0
string s = "";
for(int i=0; i<1000; i++) s += i;

// ✅ GOOD: Memory efficient (one single object)
var sb = new StringBuilder();
for(int i=0; i<1000; i++) sb.Append(i);

⚠️ Interview Trap: "GC.Collect() fixes memory issues"

Forcing GC can hurt performance, increase pauses and reduce throughput. If you need it, you probably have an allocation/leak design problem (buffers, caches, events, async, static references).

1.1 Allocations: Stack vs Heap, Span, ArrayPool

Interviewers love to check if you know when memory is allocated on stack vs heap, and how to reduce allocations in hot paths.

✅ Good patterns

  • Prefer structs for small immutable value types.
  • Use Span<T> / ReadOnlySpan<T> for slicing without allocation.
  • Use ArrayPool<T> to reuse large buffers.
  • Use StringBuilder for repeated concatenation.

🚨 Traps

  • Excessive ToList() / ToArray() in loops.
  • Allocating big arrays repeatedly (LOH pressure).
  • Capturing variables in lambdas in hot paths (closures allocate).
  • Using string interpolation in tight loops without care.

Example: Slice without allocation (Span)

// ✅ No new string allocations while parsing
ReadOnlySpan<char> input = "EUR:12345";
var currency = input[..3];
var number = input[4..];

// parse number without creating substring objects
int value = int.Parse(number);

Example: Reuse buffers (ArrayPool)

var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024 * 128); // 128KB (LOH threshold risk)
try
{
   // use buffer...
}
finally
{
   pool.Return(buffer, clearArray: true);
}

1.2 Boxing & Performance

Boxing allocates on the heap and can kill performance in hot paths.

🚨 Interview Trap: Boxing hidden in APIs

Boxing can happen when you pass a struct to an object parameter, or when you use non-generic collections.

// ❌ Boxing: int becomes object (heap allocation)
object o = (int)42;

// ✅ Avoid: use generics
List<int> nums = new();
nums.Add(42);

💡 Trap inside "logging"

Naive string concatenation / interpolation in logs can allocate even if the log level is disabled. Prefer structured logging (Serilog/MEL) with placeholders.

1.3 Finalizers, IDisposable, and GC pressure

Finalizers delay collection and promote objects to older generations. Dispose correctly.

🚨 Interview Trap: "GC will close my file"

Relying on finalizers is unpredictable. Use using / Dispose to release unmanaged resources deterministically.

Correct pattern: using / using var

// ✅ deterministic cleanup
using var fs = new FileStream("a.txt", FileMode.Open);
// read/write...

Trap: allocating finalizable objects in loops

// ❌ BAD: finalizable objects create extra GC work
for(int i=0; i<10000; i++)
{
   var fs = new FileStream("a.txt", FileMode.Open);
}

// ✅ GOOD: use using so handles are released immediately
for(int i=0; i<10000; i++)
{
   using var fs = new FileStream("a.txt", FileMode.Open);
}

1.4 Memory Leaks in .NET (Yes, it happens)

GC collects unreachable objects. Leaks happen when objects are still reachable (events, statics, caches).

🚨 Trap: Events keep references alive

If a long-lived publisher references a short-lived subscriber via an event, the subscriber cannot be collected unless unsubscribed.

⚠️ Trap: Static caches

Static dictionaries and caches grow forever if not bounded/evicted. Use size limits + eviction policies.

Event leak example

public class Publisher
{
   public event EventHandler? Tick;
   public void Raise() => Tick?.Invoke(this, EventArgs.Empty);
}

public class Subscriber
{
   public Subscriber(Publisher p)
   {
      p.Tick += OnTick; // ❗ if never unsubscribed, Subscriber stays alive
   }
   private void OnTick(object? s, EventArgs e) { }
}

// ✅ Fix: unsubscribe when done, or use WeakEvent pattern

💡 Practical fix

Use IDisposable to manage subscriptions (Rx, events), or DI scopes so disposal happens automatically.

2. Multithreading & Async

A common interview question: "What is the difference between a Task and a Thread?"

🚨 Interview Trap: Thread Starvation

Calling .Result or .Wait() on a Task can block a ThreadPool thread, leading to a deadlock in ASP.NET. Never mix sync and async code.

💡 Interview Trap: "async makes code multithreaded"

Async is about non-blocking waiting (especially I/O). It does not necessarily create new threads. Multithreading happens when work is scheduled on ThreadPool (CPU-bound) or explicit threads.

Example: Async without blocking

// 🚀 I/O Bound Task (doesn't block a thread while waiting)
public async Task<string> GetStockDataAsync()
{
   await Task.Delay(100); // Non-blocking wait
   return await _httpClient.GetStringAsync("api/stocks");
}

Trap: Fire-and-forget in ASP.NET

// ❌ BAD: request finishes, background task may be killed or unobserved
public IActionResult Post()
{
   Task.Run(async () => await _svc.DoWorkAsync());
   return Ok();
}

// ✅ Better: use IHostedService / BackgroundService or a queue

2.1 Task vs Thread (the real answer)

A Thread is an OS scheduling unit. A Task is a higher-level abstraction representing work (often on ThreadPool).

Task

  • Represents an operation (may or may not use a dedicated thread).
  • Works well with async/await.
  • Can be canceled, composed, awaited.
  • ThreadPool scheduling for CPU-bound via Task.Run.

Thread

  • OS resource, expensive to create.
  • Dedicated execution context.
  • Use rarely (special cases: long-running, affinity).
  • Too many threads → context switching overhead.

Trap: Creating threads for short tasks

// ❌ BAD: thread per request/work unit
new Thread(() => _svc.Work()).Start();

// ✅ Better: Task or ThreadPool
await Task.Run(() => _svc.Work());

2.2 ThreadPool Internals & Starvation

ThreadPool has heuristics: it injects threads gradually. Blocking ThreadPool threads can cause request queues to explode.

🚨 Classic trap: sync-over-async

In ASP.NET Core, you can still create throughput collapse if many requests block ThreadPool threads. Even without a UI SynchronizationContext, blocking hurts scalability.

Example: Blocking kills throughput

// ❌ BAD
public IActionResult Get()
{
   var data = _svc.GetAsync().Result; // blocks a ThreadPool thread
   return Ok(data);
}

// ✅ GOOD
public async Task<IActionResult> Get()
{
   var data = await _svc.GetAsync();
   return Ok(data);
}

2.3 Locks & Synchronization Primitives

Interviewers test whether you know the difference between mutual exclusion, signaling, and async-friendly locks.

Common primitives

  • lock / Monitor: mutual exclusion
  • SemaphoreSlim: permits, async-friendly WaitAsync
  • ReaderWriterLockSlim: many readers, one writer
  • Interlocked: atomic operations
  • Volatile: memory barriers visibility

🚨 Traps

  • Locking on this or string (external code can lock too).
  • Using lock in async code (risk of deadlocks + no await inside lock).
  • Not handling cancellation/timeouts in waits.

Example: SemaphoreSlim for async throttling

private readonly SemaphoreSlim _gate = new(3); // max 3 concurrent

public async Task CallExternalApiAsync()
{
   await _gate.WaitAsync();
   try
   {
      await _httpClient.GetAsync("api/data");
   }
   finally
   {
      _gate.Release();
   }
}

Example: Atomic increments (Interlocked)

private int _count = 0;

// ✅ thread-safe increment
int next = Interlocked.Increment(ref _count);

2.4 Thread-safe Collections

Big trap: List<T> and Dictionary<TKey,TValue> are not thread-safe for concurrent writes.

✅ Use these

  • ConcurrentDictionary
  • ConcurrentQueue / ConcurrentStack
  • BlockingCollection (producer/consumer)
  • ImmutableDictionary (copy-on-write model)

🚨 Traps

  • Locking around a normal Dictionary is OK, but can become contention hotspot.
  • Assuming ConcurrentDictionary is “free”: it has overhead; use only when needed.
  • Returning internal collections without copying (exposes mutable state).

Example: ConcurrentDictionary GetOrAdd

private readonly ConcurrentDictionary<string, int> _hits = new();

public int Inc(string key)
{
   return _hits.AddOrUpdate(key, 1, (_, old) => old + 1);
}

2.5 Parallelism (PLINQ, Parallel.ForEach)

Parallelism is for CPU-bound work. It can degrade performance if you parallelize I/O or tiny tasks.

🚨 Interview Trap: "Parallel always faster"

Parallelization adds overhead (partitioning, scheduling, sync, cache misses). It wins only if work is heavy enough.

Example: Parallel.ForEach for CPU heavy work

Parallel.ForEach(items, item =>
{
   // CPU-bound transform
   item.Value = Math.Sqrt(item.Value);
});

3. LINQ Optimization

LINQ is expressive but can allocate and hide complexity. Interviewers test if you know when to avoid it.

🚨 Trap: Multiple enumerations

Calling Count(), then Any(), then iterating again can enumerate multiple times. Materialize once if needed.

// ❌ BAD: enumerates multiple times
if(items.Any())
{
   var top = items.OrderByDescending(x => x.Score).First();
}

// ✅ Better: single pass when possible
bool hasAny = false;
var best = default(Item);
foreach(var x in items)
{
   hasAny = true;
   if(best == null || x.Score > best.Score) best = x;
}

💡 Interview Trap: IEnumerable vs IQueryable

IQueryable builds an expression tree and can translate to SQL (EF Core). IEnumerable runs in memory. A misplaced AsEnumerable() can move filtering to client-side.

3.1 JIT, Tiered Compilation, Inlining

JIT compiles IL to machine code. Modern .NET uses tiered compilation: fast startup first, then optimized code later.

⚠️ Trap: Micro-optimizing before profiling

JIT inlining, devirtualization, and optimizations are complex. Don’t guess: profile & benchmark.

Trap: Virtual calls can prevent inlining

// virtual dispatch may reduce inlining opportunities
public class Base { public virtual int Calc() => 1; }
public class Derived : Base { public override int Calc() => 2; }

3.2 Exceptions Cost (and when to use them)

Exceptions are expensive: stack unwinding + stack trace capture. Use them for exceptional cases, not control flow.

🚨 Trap: exceptions in hot paths

If your code throws frequently (parsing, validation), performance will collapse. Prefer TryParse or validation results.

// ❌ BAD: exceptions for flow
try { return int.Parse(s); } catch { return 0; }

// ✅ GOOD: TryParse
return int.TryParse(s, out var n) ? n : 0;

3.3 ASP.NET Core Traps (Production Reality)

Interviewers love practical backend traps: DI lifetimes, HttpClient usage, async correctness, logging, and caching.

🚨 Trap: HttpClient per request

Creating new HttpClient per call can cause socket exhaustion (TIME_WAIT). Use IHttpClientFactory.

⚠️ Trap: Wrong DI lifetime

Injecting a Scoped service into a Singleton causes issues (captured scope). Use factories or redesign lifetimes.

Correct pattern: IHttpClientFactory

// Program.cs
builder.Services.AddHttpClient("stocks", c =>
{
   c.BaseAddress = new Uri("https://api.example.com");
});

// usage
public class StocksService
{
   private readonly HttpClient _http;
   public StocksService(IHttpClientFactory f) => _http = f.CreateClient("stocks");
}

Trap: Captive dependency (Scoped into Singleton)

// ❌ BAD
builder.Services.AddSingleton<MySingleton>();
builder.Services.AddScoped<DbContext>();

public class MySingleton
{
   // scoped captured forever!
   public MySingleton(DbContext db) { }
}

// ✅ Better: use IServiceScopeFactory, or make MySingleton scoped

3.4 Profiling & Benchmarking (the senior move)

The best performance answer: "I measure first". Interviewers love tools and methodology.

💡 What to mention in interviews

  • dotnet-counters, dotnet-trace, dotnet-gcdump
  • PerfView (Windows), EventPipe
  • BenchmarkDotNet for micro-benchmarks
  • ASP.NET: logs + metrics + OpenTelemetry traces

BenchmarkDotNet example

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class LinqBench
{
   int[] data = Enumerable.Range(0, 100000).ToArray();

   [Benchmark]
   public int SumLinq() => data.Where(x => x % 2 == 0).Sum();

   [Benchmark]
   public int SumLoop()
   {
      int s = 0;
      foreach(var x in data) if(x % 2 == 0) s += x;
      return s;
      }
}

// BenchmarkRunner.Run<LinqBench>();

4. Technical Quiz (Quick Check)

Does the GC collect objects in a circular reference?

YES. The .NET GC uses mark-and-sweep (with generations). It starts from roots. If a group of objects reference each other but are unreachable from roots, they are collected.

IEnumerable vs IQueryable performance?

Huge difference.
- IEnumerable: runs in-memory (client-side).
- IQueryable: builds an expression tree and can run in DB (server-side).
Trap: calling

What is "ThreadPool starvation" in one sentence?

It’s when ThreadPool threads are blocked (sync-over-async / long blocking I/O / locks), so new work queues up and the app throughput collapses.

Why is "async void" dangerous?

async void cannot be awaited, exceptions are hard to observe/handle, and it breaks composition. Use async Task (except UI event handlers).

Does "lock" work with async/await?

You should not await inside a lock. For async coordination, use SemaphoreSlim (WaitAsync/Release) or an async lock implementation.

What is the difference between "volatile" and "Interlocked"?

volatile guarantees visibility/order for reads/writes (memory barriers) but doesn’t make compound operations atomic. Interlocked performs truly atomic operations (increment, exchange, compare-exchange).

5. Most Common Interview Traps

🚨 Trap: "new HttpClient()" everywhere

Can cause socket exhaustion. Use IHttpClientFactory or a shared client with correct lifetime.

🚨 Trap: Capturing DI scope

Injecting scoped into singleton. Fix by changing lifetime or using IServiceScopeFactory.

⚠️ Trap: EF Core N+1 queries

Lazy loading or per-row queries → explosion. Use Include, projection, batching, or split queries when appropriate.

⚠️ Trap: Returning IQueryable from services

Leaks data access concerns & lifetime issues. Prefer returning DTOs or materialized results from repository/service boundaries.

💡 Trap: "ConfigureAwait(false) everywhere"

In ASP.NET Core there is no request SynchronizationContext by default; blanket usage is not always necessary. Understand the reason: avoid capturing context in UI/legacy contexts.

✅ Senior answer that wins

“I measure first: dotnet-counters/traces + benchmarks. Then I fix allocations, blocking, and hot paths. Finally I verify with load tests.”

💡 Final tip

If you can explain why (GC pressure, ThreadPool starvation, allocations, contention) and not just “what to do”, you’ll sound senior immediately.

Built for interview prep — focus on fundamentals + real production traps.

.NET MASTER • Memory • Concurrency • Performance