.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