110 .NET Fundamentals Interview Questions and Answers (2026)

The bar for .NET interviews keeps rising. As more companies lean on .NET for high-performance APIs and AI/ML backends, interviewers expect real fluency in how the runtime, memory model, and GC actually work — not memorized definitions. Walk in shaky on fundamentals and you'll get exposed in the first ten minutes.
This is your fix: 110 questions with concise, interview-ready answers and code where it helps. They're worked Junior → Mid → Senior, so you build from the basics up to CLR internals, async, and low-allocation performance. Study them, and you'll answer like someone who's done the work.
Q1.Explain the difference between Managed and Unmanaged code. Why does .NET use a 'Managed' environment?
.NET use a 'Managed' environment?Managed code runs under the control of the .NET runtime (the CLR), which handles memory, type safety, and security; unmanaged code runs directly on the OS with no such oversight and manages its own memory.
Managed code:
Compiled to MSIL and executed by the CLR, which provides garbage collection, JIT compilation, exception handling, and type safety.
Examples: C#, F#, VB.NET.
Unmanaged code:
Compiled straight to native machine code; the developer is responsible for allocating and freeing memory.
Examples: C/C++, COM components, Win32 APIs.
Why .NET chose 'managed':
Eliminates whole classes of bugs (memory leaks, dangling pointers, buffer overruns) via automatic memory management.
Provides safety, portability, and productivity at the cost of a runtime layer.
Interop: managed code can still call unmanaged code via P/Invoke or COM interop when needed.
Q2.What is the difference between .NET Standard, .NET Core, and .NET 5/6/7/8+?
.NET Standard, .NET Core, and .NET 5/6/7/8+?.NET Standard is a specification (a set of APIs), .NET Core was the cross-platform runtime/implementation, and .NET 5+ is the unified evolution of .NET Core that merged the ecosystem under a single product.
.NET Standard:
A formal API contract, not a runtime: any platform implementing it can run a library targeting it.
Created so libraries could be shared across .NET Framework, .NET Core, Xamarin, etc.
.NET Core:
A cross-platform, open-source, modular reimplementation of .NET (versions 1.0 through 3.1).
Runs on Windows, Linux, and macOS, side by side.
.NET 5/6/7/8+:
The continuation of .NET Core (the 'Core' name was dropped); one unified platform for web, desktop, mobile, and cloud.
Even-numbered releases (6, 8) are LTS; odd-numbered (5, 7) are STS.
Mental model: Standard = the spec; Core/.NET 5+ = the actual implementation you run on.
Q3.What are the primary responsibilities of the CLR during the execution of a .NET application?
CLR during the execution of a .NET application?The CLR is the execution engine that turns MSIL into running code and manages everything around it: memory, type safety, security, and exceptions.
JIT compilation: Compiles MSIL to native machine code at runtime, method by method.
Memory management: The garbage collector allocates objects and reclaims unreferenced memory automatically.
Type safety and verification: Verifies IL and enforces the CTS so code can't access memory it shouldn't.
Exception handling: Provides a structured, language-agnostic exception model.
Security: Enforces code access and verification policies.
Assembly loading and metadata resolution: Locates, loads, and resolves assemblies and their metadata at runtime.
Q4.What is an Assembly in .NET? Explain the difference between a Library (.dll) and an Executable (.exe) in the context of modern .NET.
.NET? Explain the difference between a Library (.dll) and an Executable (.exe) in the context of modern .NET.An assembly is the fundamental unit of deployment and versioning in .NET: a compiled file containing MSIL, metadata, and a manifest. A .dll is a reusable library, while an .exe is an executable entry point, though in modern .NET that distinction has largely blurred.
What an assembly contains:
MSIL code, type metadata, a manifest (identity, version, dependencies), and optionally resources.
It is the boundary for type identity, versioning, security, and deployment.
Library (.dll): Reusable code with no entry point; referenced and loaded by other assemblies.
Executable (.exe): Has a Main entry point and can be launched directly.
Modern .NET twist:
The compiler typically emits your code into a managed .dll; the .exe produced is a lightweight native host (apphost) that bootstraps the runtime to run that .dll.
You run apps with dotnet myapp.dll or via the generated platform-specific executable.
Q5.What is .NET Standard, and is it still relevant in a post-.NET 5 world?
.NET Standard, and is it still relevant in a post-.NET 5 world?.NET Standard is a versioned specification of APIs that any .NET implementation must provide, created to let one library run across .NET Framework, .NET Core, Xamarin, and more. After .NET 5 unified the platform, its importance has shrunk but it isn't dead.
What it solved: Before unification, each runtime had different APIs; targeting netstandard2.0 let one library work on all of them.
Why it matters less now: .NET 5+ is a single unified platform, so new code can just target net8.0 directly.
When it's still relevant:
Building libraries that must also be consumed by legacy .NET Framework or older Xamarin projects.
Target netstandard2.0 for the widest reach when you need both old and new consumers.
Rule of thumb: target net8.0 for modern-only code; use netstandard2.0 only when you must support the .NET Framework era.
Q6.What are namespaces in .NET, and how do they relate to assemblies?
A namespace is a logical naming scope used to organize types and prevent name collisions; an assembly is a physical deployment/runtime unit. They are independent concepts: namespaces group names, assemblies package code.
Namespaces are logical:
They give a type a fully-qualified name like System.Collections.Generic.List<T>, avoiding clashes between identically named types.
Imported with using; purely a compile-time/organizational construct.
Assemblies are physical: A .dll or .exe that contains IL, metadata, and a manifest; the unit of versioning and deployment.
No one-to-one mapping: One namespace can span many assemblies (System lives across several), and one assembly can contain many namespaces.
Q7.Explain the fundamental differences between Value Types and Reference Types. How are they stored in memory?
Value types hold their data directly and are copied by value, while reference types hold a reference (pointer) to data on the managed heap and are copied by reference. This difference drives their memory layout, copy semantics, and equality behavior.
Value types:
Examples: int, bool, struct, and enum; derive from System.ValueType.
Assignment copies the whole value, so two variables are independent.
Cannot be null unless wrapped as Nullable<T>.
Reference types:
Examples: class, string, arrays, delegates.
Assignment copies the reference, so both variables point to the same object; can be null.
Memory storage:
A value type's data lives wherever the variable lives: on the stack as a local, or inline inside its containing object if it is a field.
A reference type's object always lives on the heap; only the reference itself may sit on the stack.
Q8.What is the difference between the stack and the heap, and which types of data are stored in each?
The stack and heap are two regions of memory the runtime uses: the stack is a fast, LIFO structure for method calls and short-lived data, while the heap is a larger pool for objects whose lifetime is managed by the garbage collector.
The stack:
Stores method call frames: local variables, parameters, and return addresses.
Holds value-type locals and the references (pointers) to heap objects.
Allocation/deallocation is just moving a pointer, so it is very fast; freed automatically when the method returns.
The heap:
Stores all reference-type objects (instances created with new for classes, arrays, strings).
Managed by the GC, which reclaims unreachable objects; more flexible but slower to allocate and collect.
Nuance: A value type that is a field of a heap object lives on the heap inline with that object: "value type" does not automatically mean "stack."
Q9.Where are value types and reference types stored (stack vs. heap), and what are the performance implications?
As a rule of thumb, value-type locals go on the stack and reference-type objects go on the heap (with only their reference on the stack), but the real distinction is lifetime, not type, and that lifetime is what drives performance.
Where things live:
Value-type local: on the stack.
Reference-type instance: object on the heap, reference on the stack (or inside another object).
Value type that is a field of a class: on the heap, embedded in that object.
Performance implications:
Stack allocation is essentially free and self-cleaning; no GC involvement.
Heap allocation costs more and adds GC pressure; lots of short-lived objects mean more collections.
Large structs copied by value can be costly; prefer in/ref parameters or keep structs small.
Practical takeaway:
Use small structs to reduce allocations on hot paths; use classes for larger, shared, or long-lived state.
Beware boxing, which silently moves a value type onto the heap.
Q10.Why are strings immutable in .NET?
Strings are immutable because a string instance can never be changed after creation: any "modification" produces a new string. This was a deliberate design choice for safety, performance, and predictability.
Thread safety: Immutable objects can be shared across threads with no locking, because nobody can mutate them.
String interning: The runtime can pool identical literals in the intern pool and reuse one instance safely only because they can't change.
Security and integrity: Paths, connection strings, and permissions passed as strings can't be altered after a check (no time-of-check/time-of-use tampering).
Hashing reliability: A string's hash is stable, so it works correctly as a Dictionary key.
The cost: Repeated concatenation creates many throwaway strings; use StringBuilder for heavy mutation.
Q11.Explain how the .NET Garbage Collector works.
The .NET GC is an automatic, generational, tracing memory manager: it tracks reachability from roots, reclaims unreachable objects, and organizes the heap into generations so most collections are fast and cheap.
Generational heap:
Gen0 holds new, short-lived objects (collected most often); survivors move to Gen1, then Gen2 (long-lived).
Based on the generational hypothesis: most objects die young, so collecting only Gen0 reclaims most garbage cheaply.
Reachability via roots: An object is live if reachable from a root (statics, stack locals, registers, handles); everything else is garbage.
Mark-sweep-compact: It marks live objects, reclaims the rest, and compacts survivors to avoid fragmentation.
Large Object Heap (LOH): Objects ≥ 85,000 bytes go on a separate heap collected with Gen2 and normally not compacted.
Modes: Workstation vs Server GC, and background GC, balance throughput against pause time depending on the workload.
Q12.What is the difference between a Task and a Thread? When would you use one over the other?
Task and a Thread? When would you use one over the other?A Thread is a low-level OS execution unit you manage directly, while a Task is a higher-level abstraction representing an asynchronous operation that typically runs on the thread pool. Prefer Task for almost all concurrency work; reach for a dedicated Thread only for specialized, long-running or special-priority scenarios.
Thread:
Maps to an actual OS thread with its own stack (~1 MB), expensive to create and tear down.
Gives fine control (priority, foreground/background, apartment state) but no built-in result or continuation model.
Task:
Represents a unit of work, usually scheduled on the shared thread pool, so threads are reused.
Supports results (Task<T>), continuations, cancellation, exceptions, and async/await.
Key distinction: A Task is not necessarily a thread: an I/O-bound async Task may use no thread at all while awaiting.
When to use each:
Use Task / Task.Run for almost all parallel and async work.
Use a raw Thread only for long-running dedicated work that shouldn't tie up the pool, or when you need specific thread settings.
Q13.What is the difference between a Delegate and an Event at the conceptual level?
Delegate and an Event at the conceptual level?A delegate is a type-safe function pointer that holds and invokes one or more methods; an event is a restricted wrapper around a delegate that gives publishers exclusive invocation rights while only letting subscribers add or remove handlers.
A delegate is the underlying mechanism:
It is a type that references methods with a matching signature; you can invoke it, assign it, and pass it around freely.
Multicast: a delegate can chain multiple methods via +=.
An event is an encapsulation layer on top of a delegate:
Outside code can only += / -= handlers; it cannot invoke the event or overwrite it with =.
Only the declaring class can raise (invoke) it, protecting the publisher/subscriber boundary.
Analogy: A delegate is like a variable holding a method; an event is like a property exposing that variable with controlled access (similar to how a property guards a field).
Q14.What is the difference between Dependency Injection (DI) and Inversion of Control (IoC)?
IoC is the broad principle of handing control of object creation and flow to something other than your own code; DI is one specific technique that implements IoC by supplying a class's dependencies from the outside rather than letting it construct them.
Inversion of Control (the principle):
A class no longer controls how its collaborators or its execution flow are wired up: a framework or container does.
Other forms of IoC exist too: events/callbacks, the template method pattern, and the framework calling your code ("don't call us, we'll call you").
Dependency Injection (the implementation):
Dependencies are passed in, usually via the constructor, instead of being newed up internally.
In .NET the DI container (IServiceCollection / IServiceProvider) resolves and injects them.
Relationship: DI is a subset of IoC. All DI is IoC, but not all IoC is DI.
Q15.What is the difference between a List<T> and a LinkedList<T>, and when would you choose one over the other?
List<T> and a LinkedList<T>, and when would you choose one over the other?A List<T> is a dynamic array (contiguous memory) optimized for indexed access and iteration; a LinkedList<T> is a doubly linked list of nodes optimized for O(1) inserts/removals once you hold a node. In practice List<T> wins almost always due to cache locality.
List<T> (array-backed):
O(1) random access by index; excellent cache locality since elements are contiguous.
Inserting/removing in the middle is O(n) (elements shift); growing reallocates and copies.
LinkedList<T> (node-backed):
O(1) insert/remove at a known node via LinkedListNode<T> references.
No indexing: finding an element is O(n) traversal; each node is a separate heap allocation with poor cache behavior.
Choosing:
Default to List<T> for indexing, iteration, and general use.
Consider LinkedList<T> only when you frequently splice/remove at positions you already hold a node for, and never index.
Q16.What is the difference between HashSet, Queue, and Stack, and what are their typical use cases?
HashSet, Queue, and Stack, and what are their typical use cases?These three differ by access pattern: HashSet<T> stores unique unordered elements for fast membership tests, Queue<T> is FIFO (first in, first out), and Stack<T> is LIFO (last in, first out).
HashSet<T>:
Hash-based set with O(1) Add/Contains/Remove; no duplicates, no defined order.
Use for deduplication, fast "have I seen this?" checks, and set operations (union, intersect).
Queue<T>:
FIFO via Enqueue / Dequeue.
Use for processing items in arrival order: work/job queues, breadth-first search.
Stack<T>:
LIFO via Push / Pop.
Use for undo history, expression evaluation, backtracking, depth-first traversal.
Q17.What is the purpose of the using statement, and how does it relate to IDisposable and try/finally?
using statement, and how does it relate to IDisposable and try/finally?The using statement is syntactic sugar that guarantees Dispose() is called on an IDisposable when the scope ends, even if an exception is thrown. The compiler expands it into a try/finally where the finally block calls Dispose().
Relationship to IDisposable: The target must implement IDisposable (or IAsyncDisposable with await using).
What it compiles to: A try/finally guaranteeing deterministic cleanup, so you don't write the boilerplate or risk forgetting it.
Forms: Block form scopes to the braces; the C# 8 using var declaration scopes to the end of the enclosing block.
Q18.When would you use a StringBuilder over a standard String, and what happens in memory during string concatenation?
StringBuilder over a standard String, and what happens in memory during string concatenation?Use StringBuilder when you build a string through many concatenations (especially in loops): strings are immutable, so each + creates a brand-new string and copies the old contents, generating garbage.
Strings are immutable:
Every concatenation allocates a new string object and copies both operands into it; the originals become garbage.
In a loop this is O(n²) copying and a flood of short-lived allocations that pressure the GC.
StringBuilder mutates an internal buffer:
It appends into a resizable char[], growing (typically doubling) only when full, so it amortizes allocations.
Call ToString() once at the end to materialize the final string.
When NOT to use it:
A small fixed number of concatenations (e.g. a + b + c) is fine as plain strings: the compiler optimizes these to String.Concat.
Prefer StringBuilder when count is unknown or large, or when building incrementally in a loop.
Q19.Explain the difference between the 'abstract' and 'virtual' keywords.
abstract' and 'virtual' keywords.Both enable polymorphism, but virtual provides a default implementation a derived class may optionally override, while abstract provides no body and forces the derived class to implement it.
virtual:
Has a body in the base class; derived classes may use override but are not required to.
Can live in a concrete (instantiable) class.
abstract:
Has no implementation; only a signature. The derived class must override it unless it is itself abstract.
Can only appear in an abstract class, which cannot be instantiated.
Shared trait: Both are resolved at runtime via the method table, so calls dispatch to the most-derived override.
Rule of thumb: Use virtual for sensible default behavior; use abstract when every subclass must supply its own.
Q20.What is the difference between 'throw' and 'throw ex'?
throw' and 'throw ex'?Use throw to rethrow and preserve the original stack trace; throw ex resets the stack trace to the current line, losing where the error actually originated.
throw (rethrow): Preserves the full original stack trace so you can see the true origin of the fault.
throw ex: Treats the exception as newly thrown from this point, overwriting the stack trace and hiding the root cause.
Guidance: Prefer plain throw when rethrowing; only wrap in a new exception (throw new ...(ex)) if you want to add context, passing the original as InnerException.
Q21.What is the role of MSIL (Microsoft Intermediate Language) and how does it enable cross-language interoperability?
MSIL (Microsoft Intermediate Language) and how does it enable cross-language interoperability?MSIL is the CPU-independent intermediate language that every .NET compiler emits; because all languages compile to the same IL and metadata, they can interoperate seamlessly.
What MSIL is:
A low-level, stack-based, platform-neutral instruction set (also called CIL or IL).
Stored in the assembly alongside metadata describing types and members.
How it enables cross-language interop:
C#, F#, and VB.NET all compile down to the same MSIL, so a type written in one language is just a type to another.
Shared rules in the CTS and CLS guarantee the types are mutually understandable.
Portability: the same IL runs anywhere the CLR exists, since the JIT targets the local CPU.
Q22.Can you explain the execution flow of a .NET application from source code to machine code?
.NET application from source code to machine code?Source code is compiled once to MSIL in an assembly, then the CLR JIT-compiles that IL into native machine code at runtime and executes it.
Write source code in a .NET language (C#, F#, VB.NET).
The language compiler (e.g. Roslyn) produces an assembly (.dll/.exe) containing MSIL plus metadata.
At runtime the CLR loads the assembly and reads its metadata.
The JIT compiler translates IL methods to native machine code on first call.
The CPU executes the native code, while the CLR manages memory, security, and exceptions.
Note: AOT compilation (e.g. ReadyToRun or Native AOT) can do this translation ahead of time to skip JIT at startup.
Q23.What is the Common Type System (CTS) and why is it necessary for a multi-language platform like .NET?
Common Type System (CTS) and why is it necessary for a multi-language platform like .NET?The Common Type System (CTS) is the standard that defines how types are declared, used, and managed in the runtime, ensuring every .NET language shares one consistent type model.
What it defines:
The rules for all types: value types vs. reference types, inheritance, visibility, and members.
A common root: every type ultimately derives from System.Object.
Why it's necessary:
Without a shared type model, a C# int and an F# integer couldn't be passed between languages safely.
It guarantees type safety and lets the CLR verify code uniformly.
Related: the CLS (Common Language Specification) is a subset of the CTS that languages must support to be fully interoperable.
Q24.What is an Assembly Manifest and what information does it contain?
An assembly manifest is the metadata block embedded in every .NET assembly that describes the assembly to itself and to the runtime: it is the assembly's self-describing contents, making .NET assemblies fully self-contained units.
Assembly identity: Simple name, version number, culture, and (if present) the public key token / strong-name signature.
List of files in the assembly: Most assemblies are single-file, but a multi-file assembly's manifest names every module and its hash.
Type reference information: Maps exported types to the file/module that implements them.
References to other assemblies: Names and versions of dependencies, so the runtime can resolve and load them.
Why it matters: It is the basis for versioning and the unit of deployment/security; tools like ildasm or AssemblyName read it.
Q25.What is Boxing and Unboxing? What are the performance implications of these operations?
Boxing wraps a value type into an object on the heap so it can be treated as a reference type; unboxing extracts the value back out. Both involve heap allocation and copying, so they cost performance and GC pressure when done in hot paths.
Boxing:
Allocates a new object on the heap and copies the value into it, e.g. object o = 42;.
Happens implicitly when a value type is assigned to object, an interface, or used with non-generic collections.
Unboxing:
Explicit cast back, e.g. int i = (int)o;; copies the value out and verifies the type at runtime.
Wrong cast type throws InvalidCastException.
Performance implications:
Each box is a heap allocation that the GC must later collect, hurting throughput in loops.
Avoid with generics (List<int> instead of ArrayList) and generic interfaces like IEquatable<T>.
Q26.What is the difference between a class and a record at the runtime level?
class and a record at the runtime level?At the runtime level there is no separate "record" type: a record is still compiled to an ordinary class (or struct with record struct). The difference is compiler-generated members, not a distinct CLR construct.
Same runtime category: A record emits a normal reference type (or value type for record struct); the IL declares a class with extra members.
Compiler-synthesized members:
Value-based Equals and GetHashCode that compare members instead of references.
A Clone-style method backing the with expression for non-destructive copies.
A formatted ToString and an EqualityContract property.
Semantics differ, identity model differs: Plain classes have reference equality by default; records have value equality.
Takeaway: "record" is a C# language feature, not a new metadata kind: the JIT/CLR sees just a class or struct.
Q27.What is Nullable<T> and how are nullable value types represented at the runtime level?
Nullable<T> and how are nullable value types represented at the runtime level?Nullable<T> is a value type (a struct) that lets a value type also represent "no value." It is not a reference and is not boxed unless you box it: it simply wraps a T plus a bool flag.
It is a struct, not magic: Nullable<T> holds a HasValue bool and a Value of type T; int? is just syntax for Nullable<int>.
No null on the stack: It lives inline like any struct; HasValue == false represents "null," no actual null pointer involved.
Special boxing behavior: Boxing a Nullable<T> with no value yields a real null reference, not a boxed struct; with a value it boxes the underlying T.
Distinct from nullable reference types: Nullable reference types (string?) are a compile-time annotation only; Nullable<T> is a real runtime type.
Q28.What is the Large Object Heap (LOH), and why is it managed differently than the Small Object Heap (SOH)?
LOH), and why is it managed differently than the Small Object Heap (SOH)?The LOH is a separate managed heap for large objects (85,000 bytes or more by default). It is treated differently mainly because compacting (moving) large objects is expensive, so the GC normally sweeps it without compacting.
What goes there: Objects ≥ 85,000 bytes, typically big arrays, large strings, or buffers.
Collected with Gen 2: The LOH is logically part of Gen 2, so it is only reclaimed during a full (Gen 2) collection, which is the most costly.
Not compacted by default: Moving large blocks is costly, so the GC sweeps and leaves free gaps, which can cause fragmentation over time.
Fragmentation risk: Gaps may not fit new large allocations, growing memory even with free space available.
Mitigations: Pool/reuse large buffers (e.g. ArrayPool<T>), or force compaction via GCSettings.LargeObjectHeapCompactionMode when truly needed.
Q29.Can you explain the three generations (0, 1, and 2) of the .NET Garbage Collector and why it uses a generational approach?
The GC divides the managed heap into three generations based on object age. The core insight (the "generational hypothesis") is that most objects die young, so collecting young objects frequently and old objects rarely is far cheaper than scanning the whole heap every time.
Gen 0: newest, short-lived objects: Collected most often and very fast (small region); survivors are promoted to Gen 1.
Gen 1: survived one collection: Acts as a buffer between short-lived and long-lived objects; survivors are promoted to Gen 2.
Gen 2: long-lived objects: Collected least often; a Gen 2 collection is a "full" GC that scans the whole heap (including the LOH) and is the most expensive.
Why generational:
Frequent cheap Gen 0 sweeps reclaim most garbage without touching old, stable objects.
Collecting a younger generation also collects all younger ones (Gen 2 includes Gen 1 and 0).
Q30.How does the .NET Garbage Collector decide when to run? What are the primary triggers?
The GC runs on demand, not on a fixed schedule. The dominant trigger is allocation pressure: when a generation's allocation budget is exceeded, a collection fires. Other triggers include system memory pressure and explicit requests.
Generation budget exceeded (primary): Each generation has a size threshold; when Gen 0 allocations fill its budget a Gen 0 collection runs, and the budget self-tunes based on survival rates.
Low system memory: The OS signaling memory pressure prompts the GC to reclaim sooner.
Explicit call: GC.Collect() forces a collection (generally discouraged in production code).
Other conditions: AppDomain unload or process shutdown, and LOH allocations contributing to the Gen 2 budget.
Q31.What is a 'Memory Leak' in a managed environment like .NET? Give an example of how one might occur.
In managed .NET a memory leak isn't unfreed memory (the GC handles that): it is unintentionally keeping objects reachable so the GC can never collect them. Memory grows because a live reference root still points to objects you no longer use.
Definition: Objects stay rooted (reachable from a static, a long-lived object, or an active stack frame) indefinitely.
Most common cause: event handlers: A long-lived publisher holds a reference to a subscriber via its delegate; if you never unsubscribe, the subscriber can't be collected.
Other classic causes:
Static collections that only ever grow (caches without eviction).
Undisposed objects holding unmanaged resources or timers that keep callbacks rooted.
Captured variables in long-lived closures or background tasks.
Remedies: Unsubscribe from events, dispose with IDisposable, bound caches, and consider WeakReference for non-owning references.
Q32.How does the .NET Thread Pool manage worker threads, and what is 'Thread Pool Starvation'?
The thread pool is a managed set of reusable worker threads that run queued work items, avoiding the cost of creating a thread per task. Thread pool starvation happens when all pool threads are blocked, so queued work can't run and the pool only adds new threads slowly, causing severe latency or deadlock.
How it manages threads:
Maintains a pool of worker (and I/O completion) threads and reuses them for queued work items.
Keeps a baseline minimum count, then injects extra threads gradually (roughly one per ~500ms) using a hill-climbing heuristic to maximize throughput.
Powers Task.Run, async continuations, timers, and similar work.
What starvation is: All threads are tied up (often blocked on synchronous waits), so pending items sit in the queue until the slow thread-injection catches up.
Typical cause: sync-over-async: Blocking on async code with .Result or .Wait() consumes a pool thread while waiting for a continuation that needs another pool thread.
How to avoid it:
Use async/await all the way down; never block on async calls.
Keep CPU-bound long work off the shared pool, or tune ThreadPool.SetMinThreads only as a stopgap.
Q33.Explain the 'Mark-Sweep-Compact' algorithm used by the GC.
GC.Mark-Sweep-Compact is the three-phase algorithm the GC uses to reclaim memory: it marks reachable objects, sweeps (reclaims) the unreachable ones, then compacts survivors to remove the gaps.
Mark:
Starting from roots (statics, locals on the stack, CPU registers, GC handles), the GC walks the object graph and flags every reachable object as live.
Anything not reached is garbage by definition.
Sweep: The memory occupied by unmarked (unreachable) objects is reclaimed and made available.
Compact:
Surviving objects are moved together to one end of the heap, eliminating fragmentation so new allocations are fast (just bump a pointer).
Because objects move, the GC updates all references to point to new addresses.
Note: Compaction is the expensive part, so the GC sometimes only sweeps; the Large Object Heap is swept but not normally compacted.
Q34.Why is it considered a bad practice to call GC.Collect() manually in a production application?
GC.Collect() manually in a production application?Calling GC.Collect() manually is discouraged because the GC is self-tuning, and forcing a collection usually hurts performance and can prematurely promote objects to higher generations.
It defeats the GC's own heuristics: The GC dynamically tunes collection frequency based on allocation patterns and memory pressure; a manual call overrides this with worse timing.
It can cause premature promotion: A full collect promotes short-lived objects that happened to survive into Gen1/Gen2, where they're collected far less often (the opposite of what you want).
It's expensive: A full GC.Collect() can pause threads and walk the whole heap, adding latency spikes under load.
Rare legitimate uses: After a one-time large allocation is freed (e.g. finishing a big batch job), or in benchmarking/leak diagnostics, not in normal request paths.
Q35.Why is async void considered dangerous in .NET, and what is the one exception where it is acceptable?
async void considered dangerous in .NET, and what is the one exception where it is acceptable?async void is dangerous because the caller gets no Task to await, so exceptions can't be caught and the method can't be awaited for completion; the one acceptable use is top-level event handlers, which the framework requires to return void.
Why it's dangerous:
Exceptions are raised on the synchronization context, not propagated to the caller, often crashing the process.
There's no Task to await, so callers can't know when it finished (fire-and-forget).
It's hard to test and to compose with other async code.
The exception: event handlers:
UI/event signatures (e.g. void Button_Click(object s, EventArgs e)) must return void, so async void is the sanctioned pattern there.
Even then, wrap the body in try/catch so exceptions don't escape.
Rule of thumb: Always return Task (or Task<T>) instead of void everywhere except event handlers.
Q36.What is the difference between Task and ValueTask? When should you prefer ValueTask?
Task and ValueTask? When should you prefer ValueTask?Task is a reference type representing any async operation, while ValueTask is a struct that can wrap either an already-available result or a Task, avoiding a heap allocation on the common synchronous-completion path. Prefer ValueTask in hot paths that frequently complete synchronously, but default to Task otherwise.
Task:
A class, so returning one allocates on the heap (though completed common cases are cached).
Flexible: can be awaited multiple times, combined with WhenAll, stored, etc.
ValueTask:
A struct that holds a result inline when the operation completes synchronously, avoiding allocation.
Backs APIs like caching layers and IAsyncEnumerable where most calls return immediately.
ValueTask restrictions:
Must be awaited only once and not awaited concurrently; don't store it or call .Result before completion.
If you need to await multiple times, convert with AsTask().
When to prefer ValueTask:
High-frequency methods that often complete synchronously and where allocation overhead is measured to matter.
Otherwise use Task: it's simpler and harder to misuse.
Q37.What is the difference between Task.WhenAll and Task.WaitAll?
Task.WhenAll and Task.WaitAll?Both wait for many tasks to complete, but Task.WhenAll is asynchronous (returns a Task you await) while Task.WaitAll is synchronous (blocks the calling thread until all finish).
Task.WhenAll:
Returns a Task immediately; you await it, freeing the thread while tasks run.
Preferred in async code; non-blocking and scalable.
Task.WaitAll:
Blocks the current thread until every task completes, tying up that thread.
Can cause deadlocks in contexts with a SynchronizationContext (sync-over-async).
Exception handling differs:
await Task.WhenAll rethrows the first exception (others are on the aggregate Task).
Task.WaitAll throws an AggregateException wrapping all failures.
Q38.What actually happens when you 'await' a Task? Does it block the thread?
await' a Task? Does it block the thread?No, await does not block the thread. It suspends the method, returns control to the caller, and registers the rest of the method as a continuation that runs when the awaited task completes; the thread is freed in the meantime.
What the compiler does:
It rewrites the method into a state machine; await is a suspension point.
If the task is already complete, execution simply continues synchronously (no suspension cost).
What happens on suspension:
The method returns its Task to the caller; the calling thread is free to do other work (e.g. handle other requests or UI events).
When the awaited task finishes, the continuation is scheduled (on the captured context, or the thread pool if ConfigureAwait(false)).
Contrast with blocking: .Result or .Wait() DO block: the thread sits idle until completion, which is exactly what await avoids.
Q39.What is the difference between 'async void' and 'async Task'?
async void' and 'async Task'?Both let you write asynchronous methods, but async Task returns an awaitable handle so callers can await it and observe completion/exceptions, whereas async void is fire-and-forget: it cannot be awaited and its exceptions escape to the top level.
async Task:
Awaitable; the caller can wait for it and compose it.
Exceptions are captured on the returned task and rethrown when awaited.
async void:
Not awaitable; the caller has no way to know when it finishes.
An unhandled exception is raised on the SynchronizationContext and typically crashes the process.
The only legitimate use of async void: Top-level event handlers (e.g. a UI Button_Click) that must match a void delegate signature.
Rule of thumb: Default to async Task; use async void only for event handlers.
Q40.What is 'sync-over-async' and why is it considered an anti-pattern in .NET?
.NET?Sync-over-async is calling asynchronous code and then blocking on it synchronously (via .Result, .Wait(), or .GetAwaiter().GetResult()). It defeats the purpose of async, wastes threads, and can deadlock, which is why it is an anti-pattern.
It wastes the very thread async tries to free: The thread blocks idle waiting for the task instead of doing other work, hurting scalability (thread pool starvation under load).
It can deadlock: In a context that captures a SynchronizationContext (UI, classic ASP.NET), blocking the context thread prevents the continuation from resuming.
It hides exceptions awkwardly: Blocking wraps errors in AggregateException, complicating handling.
The fix: Be async all the way: propagate async/await up the call chain rather than blocking.
Q41.How does the lock statement work under the hood, and how does it relate to Monitor.Enter/Exit?
lock statement work under the hood, and how does it relate to Monitor.Enter/Exit?The lock statement is syntactic sugar over Monitor.Enter and Monitor.Exit wrapped in a try/finally, giving a single thread mutually exclusive access to a block while guaranteeing the lock is released even on exception.
What the compiler generates:
lock(obj){...} expands to Monitor.Enter(obj, ref lockTaken) then a finally that calls Monitor.Exit if the lock was taken.
The finally ensures release even if the body throws.
How exclusivity works:
Every object has a sync block index pointing to a monitor; only one thread can own it at a time.
Other threads block until the owner exits; the lock is reentrant for the same thread.
Best practices:
Lock on a private, dedicated readonly object, never on this, a Type, or a string.
You cannot await inside a lock; use SemaphoreSlim for async mutual exclusion.
Q42.What is the Interlocked class used for, and why are atomic operations important in multithreaded code?
Interlocked class used for, and why are atomic operations important in multithreaded code?The Interlocked class performs atomic operations on shared variables (increment, decrement, add, exchange, compare-and-swap) without taking a lock. Atomicity matters because seemingly simple operations like i++ are actually read-modify-write sequences that can be interrupted by another thread, producing lost updates.
Why atomicity matters:
count++ = read, add one, write back; two threads can read the same value and one update is lost.
Atomic ops complete as a single indivisible step that no other thread can observe mid-flight.
Common methods:
Interlocked.Increment / Decrement / Add for counters.
Interlocked.Exchange to atomically set and return the old value.
Interlocked.CompareExchange for lock-free algorithms (set only if the value still matches).
Benefit: faster than a lock for single-variable updates and includes a full memory barrier, so the change is visible to other threads.
Q43.What is a race condition, and how do you make shared state thread-safe in .NET?
A race condition occurs when the correctness of a program depends on the unpredictable timing or interleaving of multiple threads accessing shared mutable state, so the same code can yield different results. You make shared state thread-safe by serializing access (locks), using atomic operations, or avoiding shared mutable state altogether.
Classic example:
Two threads run balance += 100; both read the old balance and one deposit is lost.
Check-then-act bugs (if (x == null) x = new()) are also races.
Ways to make state thread-safe:
lock / Monitor: serialize a critical section so only one thread enters at a time.
Interlocked: atomic updates for single primitive variables.
Concurrent collections: ConcurrentDictionary, ConcurrentQueue handle locking internally.
Immutability / no sharing: immutable objects or per-thread data (e.g. ThreadLocal) have no race by design.
Lock discipline: Always lock on a private readonly object, keep the section short, and acquire multiple locks in a consistent order to avoid deadlock.
Q44.What is IAsyncEnumerable and how do async streams work with 'await foreach'?
IAsyncEnumerable and how do async streams work with 'await foreach'?IAsyncEnumerable<T> represents an asynchronous stream: a sequence whose elements are produced over time and fetched with awaits, consumed via await foreach. It combines lazy iteration (like IEnumerable) with async I/O, so you can stream results without buffering everything or blocking a thread.
How it works:
A method returning IAsyncEnumerable<T> uses yield return inside an async iterator, awaiting between items.
await foreach awaits MoveNextAsync() on each iteration, suspending until the next element is ready.
When to use it: Paging an API, reading DB rows, or processing a file line-by-line: handle items as they arrive instead of materializing a full list.
Cancellation: Pass a token with [EnumeratorCancellation] and WithCancellation(token) on the consumer side.
Q45.Can you explain the three service lifetimes in .NET Dependency Injection (Transient, Scoped, Singleton) and the risk of a captive dependency?
Transient, Scoped, Singleton) and the risk of a captive dependency?The three lifetimes control how often the DI container creates an instance of a service. Transient is created every time it's requested, Scoped once per scope (typically per web request), and Singleton once for the application's lifetime. A captive dependency is when a longer-lived service holds a reference to a shorter-lived one, accidentally extending its lifetime.
Transient: New instance per injection/resolve; good for lightweight, stateless services.
Scoped:
One instance per scope; in ASP.NET Core each HTTP request is a scope, so the same instance is shared within that request.
Ideal for per-request state like a DbContext.
Singleton: One instance for the whole app; must be thread-safe because many requests share it.
Captive dependency risk:
Injecting a Scoped (or Transient) service into a Singleton pins it alive for the app's lifetime, causing stale data, leaks, or threading bugs (e.g. a shared DbContext).
The framework's scope validation throws on this in development; to use scoped work from a singleton, inject IServiceScopeFactory and create a scope on demand.
Q46.What is the 'Generic Host' in .NET, and how does it manage the application lifecycle?
The Generic Host (IHost) is a container that bootstraps and manages a .NET application's lifetime, dependency injection, configuration, and logging in a unified way, for any app type (web, worker service, console). It owns startup and graceful shutdown and runs registered hosted services for the duration of the app.
What it wires up: The DI container, configuration (IConfiguration), logging, and IHostEnvironment, all built via Host.CreateDefaultBuilder / HostApplicationBuilder.
Lifecycle management:
On StartAsync it starts every registered IHostedService; on shutdown it calls StopAsync in reverse order.
Listens for shutdown signals (Ctrl+C, SIGTERM) and supports graceful shutdown with a configurable timeout.
IHostApplicationLifetime exposes ApplicationStarted/Stopping/Stopped events.
Why it matters: One consistent model: ASP.NET Core, background BackgroundService workers, and CLI tools all share the same hosting infrastructure.
Q47.What is the Options pattern and how does IConfiguration bind configuration into strongly-typed objects?
IConfiguration bind configuration into strongly-typed objects?The Options pattern binds sections of configuration to plain strongly-typed classes (POCOs) and exposes them through interfaces like IOptions<T>, giving you type-safe, testable access to settings instead of reading magic string keys everywhere.
How binding works:
IConfiguration reads providers (appsettings.json, env vars, etc.) into a key/value tree.
Configure<T> or Bind() maps a config section onto a class by matching property names to keys (reflection-based).
The consumption interfaces:
IOptions<T>: singleton, value computed once, no reload.
IOptionsSnapshot<T>: scoped, recomputed per request, picks up changes.
IOptionsMonitor<T>: singleton with live reload and change notifications.
Benefits: decoupling from config keys, validation via ValidateDataAnnotations(), and easy unit testing by constructing the POCO directly.
Q48.Why is constructor injection preferred over the service locator pattern in .NET DI?
.NET DI?Constructor injection makes dependencies explicit and resolved by the container, while the service locator hides them behind a global provider that classes reach into. Constructor injection produces clearer, more testable, and harder-to-misuse code, which is why it's considered the default and the service locator an anti-pattern.
Dependencies are honest: A constructor signature lists exactly what the class needs; a service locator hides those needs inside method bodies.
Fail fast and validated: Missing registrations throw at construction/startup; with a locator you discover failures at runtime when GetService() is called.
Testability: You just pass mocks/fakes to the constructor; the locator forces you to stub a whole IServiceProvider.
Encourages good design: A bloated constructor visibly signals a class doing too much (SRP violation); the locator hides that smell.
Caveat: the locator still has niche uses (e.g. dynamic resolution in frameworks), but it shouldn't be the default for application code.
Q49.What is the difference between IEnumerable and IQueryable? Specifically, how do they handle 'Deferred Execution'?
IEnumerable and IQueryable? Specifically, how do they handle 'Deferred Execution'?IEnumerable<T> works with in-memory objects and executes LINQ in your process, while IQueryable<T> builds an expression tree that a provider (e.g. EF Core) translates into a remote query like SQL. Both defer execution, but IQueryable defers the translation and runs filtering at the data source.
IEnumerable<T>:
LINQ-to-Objects: operates on data already in memory using compiled delegates (Func<T>).
If used against a DB, it fetches rows first, then filters in memory: potentially huge waste.
IQueryable<T>:
Stores operations as an Expression tree that the provider translates to the target query language.
Filtering, paging, and projection run server-side, so only needed rows come back.
Deferred execution in both: Nothing runs until you enumerate (foreach, ToList(), Count()). For IQueryable, that's when the SQL is generated and sent.
Gotcha: calling AsEnumerable() too early switches further operators to client-side evaluation.
Q50.Explain the role of the CancellationToken and why it is essential for cooperative cancellation.
CancellationToken and why it is essential for cooperative cancellation.A CancellationToken is a lightweight signal passed into async/long-running operations so they can be asked to stop. Cancellation is cooperative: the token only requests cancellation, and the running code must check it and decide to honor it, which keeps shutdown clean and avoids forcibly killing threads.
How the mechanism works:
A CancellationTokenSource owns the state; its .Token is handed to consumers, and Cancel() trips it.
Code observes it via token.IsCancellationRequested or token.ThrowIfCancellationRequested().
Why cooperative:
Hard-aborting threads leaves locks, files, and state corrupted; cooperative cancellation lets work unwind safely.
Most async APIs accept a token and throw OperationCanceledException when tripped.
Why it's essential:
Frees resources promptly: stop a DB query or HTTP call when the user navigates away or a request times out.
ASP.NET Core supplies HttpContext.RequestAborted so handlers stop when the client disconnects.
Best practice: flow the token through every layer of an async call chain rather than swallowing it.
Q51.Explain 'Deferred Execution' in LINQ. Why is it important?
LINQ. Why is it important?Deferred execution means a LINQ query defines what to do but doesn't actually run until you enumerate its results. The query is a recipe, not the meal: it executes lazily at the point of iteration, not at the point of declaration.
When execution actually happens:
On enumeration: foreach, or a materializing operator like ToList(), ToArray(), Count(), First().
Operators like Where and Select just build the pipeline; they don't run yet.
Why it's important:
Efficiency: you can compose filters and only the final query runs, and the source can short-circuit (e.g. First() stops early).
Freshness: re-enumerating reflects the current state of the underlying source.
Composability: queries can be built up across methods before executing once.
Pitfalls:
Multiple enumeration re-runs the query (and re-hits the DB) each time: materialize with ToList() if you'll reuse results.
Captured variables or a disposed DbContext can cause surprises because execution is delayed.
Q52.How does a Dictionary<TKey, TValue> work internally? What happens during a hash collision?
Dictionary<TKey, TValue> work internally? What happens during a hash collision?A Dictionary<TKey, TValue> is a hash table: it maps a key's hash code to a bucket, then stores the entry in an array of slots. Lookups, inserts, and deletes are amortized O(1) because the hash narrows the search to one bucket.
Internal layout:
Two arrays: a buckets array (indices into entries) and an entries array holding hash, key, value, and a next index.
Bucket index is computed as hashCode % buckets.Length.
Collision handling (separate chaining):
When two keys land in the same bucket, entries form a linked chain via the next field; lookup walks the chain comparing keys with EqualsComparer.
.NET uses chaining (not open addressing): collisions degrade that bucket to O(n) in the worst case.
Equality and hashing:
Keys are matched with GetHashCode() to find the bucket, then Equals() to confirm the exact key.
A bad GetHashCode() (many collisions) turns the dictionary into a linked list, killing performance.
Resizing: When the load grows, capacity grows to the next prime and all entries are rehashed into the new buckets.
Q53.What are the performance trade-offs of using LINQ, and when might a foreach loop be significantly faster?
LINQ, and when might a foreach loop be significantly faster?LINQ trades raw performance for readability and expressiveness. For most code the cost is negligible, but in hot paths it adds overhead from delegate calls, iterator allocations, and deferred-execution machinery that a plain foreach avoids.
Where the overhead comes from:
Each operator allocates an iterator/state machine object and invokes a delegate (lambda) per element, which the JIT often can't inline.
Captured variables create closure allocations; chained operators allocate at each stage.
Boxing can occur when value types flow through non-generic or interface paths.
When foreach is significantly faster:
Tight, hot loops over large collections (millions of iterations) where allocations and delegate calls dominate.
Iterating a known concrete type like List<T> or an array, where foreach uses a struct enumerator with no heap allocation.
Nuance:
LINQ's deferred execution and Where/Take short-circuiting can avoid work and beat a naive loop.
Don't micro-optimize blindly: measure with a benchmark before replacing LINQ for speed.
Q54.What are 'Frozen Collections' (introduced in .NET 8), and how do they differ from 'Immutable Collections'?
Frozen collections (FrozenDictionary<TKey,TValue> and FrozenSet<T>) are read-only collections built once and optimized for extremely fast repeated reads. Like immutable collections they can't be changed, but the key difference is intent: frozen pays an expensive build cost to make lookups as fast as possible, while immutable optimizes for cheap, safe copy-on-write mutation.
Frozen collections:
Created via ToFrozenDictionary()/ToFrozenSet(); the build analyzes the data to pick an optimal internal layout.
Construction is slow and one-time; reads are faster than a regular Dictionary.
Truly read-only: no add/remove APIs at all.
Immutable collections:
e.g. ImmutableDictionary, ImmutableList; "mutation" returns a new instance sharing structure with the old.
Optimized for safe, frequent changes; lookups are slower than a frozen or regular dictionary.
When to use frozen: Build-once, read-many scenarios: lookup tables, caches, and config loaded at startup and never changed.
Q55.What are the concurrent collections like ConcurrentDictionary, and how do they differ from using a lock around a regular collection?
ConcurrentDictionary, and how do they differ from using a lock around a regular collection?Concurrent collections (ConcurrentDictionary, ConcurrentQueue, ConcurrentBag) are thread-safe by design, using fine-grained locking or lock-free techniques internally. They differ from wrapping a regular collection in a lock by allowing much higher concurrency and offering atomic compound operations.
Finer-grained synchronization:
ConcurrentDictionary partitions into multiple lock regions (striping), so threads touching different buckets don't contend.
Reads are typically lock-free; a single lock around a Dictionary serializes every access, becoming a bottleneck.
Atomic compound operations:
Methods like GetOrAdd, AddOrUpdate, and TryUpdate do check-then-act atomically; with a manual lock you'd have to write that yourself.
Note the value factory in GetOrAdd may run more than once under contention, though only one result is stored.
Trade-offs:
Higher per-operation overhead and memory than a plain collection; only worth it under real concurrent access.
Enumeration gives a moment-in-time snapshot and may not reflect concurrent changes.
A manual lock can still be better when you need to make several operations atomic as a single unit.
Q56.What is PLINQ and Parallel.For, and when is parallelizing work actually beneficial?
PLINQ and Parallel.For, and when is parallelizing work actually beneficial?PLINQ (AsParallel()) and Parallel.For spread work across multiple CPU cores. PLINQ is declarative parallel querying; Parallel.For is an imperative parallel loop. Both help only when work is CPU-bound and large enough that parallelism outweighs its coordination cost.
PLINQ:
Add AsParallel() to a LINQ query to partition the source and run operators across threads.
Order isn't preserved unless you call AsOrdered() (which adds cost).
Parallel.For / Parallel.ForEach: Run loop iterations concurrently; best when each iteration is independent and substantial.
When parallelizing actually pays off:
CPU-bound work (computation, transforms) with enough items per iteration to amortize thread overhead.
Not for I/O-bound work: use async/await instead, which scales without burning threads.
Pitfalls:
Small workloads run slower due to partitioning and synchronization overhead.
Shared mutable state needs synchronization or thread-local aggregation; otherwise you get race conditions.
Q57.What are 'NuGet' packages, and how does the .NET build system (MSBuild) resolve version conflicts?
NuGet' packages, and how does the .NET build system (MSBuild) resolve version conflicts?NuGet is .NET's package manager: a NuGet package (.nupkg) bundles compiled assemblies, metadata, and dependency info that you reference in a project. MSBuild (via the NuGet restore step) builds a full dependency graph and, when the same package appears at multiple versions, applies the "nearest-wins" rule and unification to pick a single version.
What a NuGet package is:
A zip with a .nuspec manifest, target-framework-specific DLLs, and declared dependencies.
Referenced through <PackageReference> in the .csproj (PackageReference model).
How conflicts are resolved:
Nearest wins: the version closest to your project in the dependency graph takes precedence over a transitive one further away.
Lowest applicable: among requested ranges, NuGet picks the lowest version that satisfies all constraints (not the highest).
Unification: only one version of a given package id ends up in the output; the resolved set is recorded in project.assets.json.
Overriding and diagnosing:
A direct <PackageReference> in your project beats a transitive one, so you pin versions explicitly to break conflicts.
Use Central Package Management (Directory.Packages.props) to align versions solution-wide; build warning NU1605 flags downgrades.
Q58.What is the difference between 'Strong Naming' an assembly and 'Digitally Signing' it?
They solve different problems: strong naming establishes a unique runtime identity for binding, while digital (Authenticode) signing establishes publisher trust and integrity. Strong naming is not a security mechanism; Authenticode is.
Strong naming:
Purpose: a unique, versioned identity (the PublicKeyToken) so the runtime can distinguish and bind assemblies.
The public key is embedded in the assembly, so anyone can read it; it proves nothing about who built it.
Verified by the CLR at load time (when applicable), not by the OS.
Digital (Authenticode) signing:
Purpose: prove publisher identity and that the file wasn't tampered with, using a certificate from a trusted CA.
Verified against the OS certificate trust chain; drives "unknown publisher" warnings and antivirus reputation.
Key contrast:
Strong naming answers "which exact assembly is this?"; signing answers "can I trust who shipped it?".
They are independent: an assembly can be both, either, or neither.
Q59.What is Reflection, and what are its primary use cases and performance tradeoffs?
Reflection is the ability to inspect type metadata at runtime and to read members, create instances, and invoke methods dynamically without knowing the types at compile time. It is powerful and flexible but slower than direct calls because it bypasses compile-time binding and JIT optimizations.
What it does:
Reads metadata via Type, MethodInfo, PropertyInfo, etc., obtained from typeof or GetType().
Creates and invokes dynamically with Activator.CreateInstance and MethodInfo.Invoke.
Primary use cases:
Serializers, ORMs, DI containers, and test runners that must work over arbitrary types.
Plugin systems and reading custom attributes.
Performance tradeoffs:
Member lookup and Invoke are far slower than direct calls (metadata search, boxing, security/arg checks).
Mitigate by caching MemberInfo and compiling to delegates via Expression trees or Delegate.CreateDelegate.
Hurts trimming/AOT because the linker can't see dynamically referenced members.
Q60.What are attributes in .NET, and how does the runtime or a library use them via reflection?
Attributes are declarative metadata you attach to code elements (classes, methods, properties, parameters) by deriving from System.Attribute. They do nothing on their own: they are stored in assembly metadata, and some consumer (the runtime, a library, or a tool) reads them via reflection and acts on them.
What they are:
Classes ending in Attribute, applied with square-bracket syntax like [Obsolete] or [Required].
Constructor args/named properties become the metadata values.
How they're consumed:
A consumer calls GetCustomAttributes on a Type or MemberInfo to retrieve instances and read their data.
Examples: model validation, ASP.NET routing ([HttpGet]), serialization names, test discovery.
Runtime vs compile-time:
Most are read via reflection at runtime; a few influence the compiler directly ([Obsolete], [Conditional]).
Control their usage with [AttributeUsage] (targets, AllowMultiple, inheritance).
Q61.What are generic type constraints, and how does the runtime enforce them?
Generic constraints (where clauses) restrict which type arguments a generic can accept, giving the compiler guarantees about what operations are valid on the type parameter. They are enforced primarily at compile time, but the CLR also records them in metadata and validates them at JIT/load time.
Common constraint kinds:
where T : class / where T : struct (reference vs value type).
where T : new() (public parameterless constructor).
where T : BaseClass or where T : IInterface (must derive from / implement).
where T : U (one parameter must be convertible to another), plus notnull, unmanaged.
Why they matter: Without a constraint, T is treated as object; constraints unlock members (e.g. calling interface methods) and operations (new T()).
How enforcement works:
The compiler rejects type arguments that violate a constraint and only lets you use members the constraint guarantees.
Constraints are emitted into IL metadata, so the CLR re-verifies them when a closed generic is loaded/JITed (e.g. via reflection or MakeGenericType), throwing if violated.
Q62.Explain the difference between the Finalize method and the Dispose pattern. Why should you avoid using Finalizers if possible?
Finalize method and the Dispose pattern. Why should you avoid using Finalizers if possible?A finalizer (~ClassName, which overrides Object.Finalize) is a non-deterministic safety net the GC calls to release unmanaged resources; the Dispose pattern (IDisposable.Dispose) is deterministic cleanup the caller triggers explicitly. You prefer Dispose because finalizers are slow and unpredictable.
Finalizer:
Runs on a dedicated finalizer thread at a time the GC chooses, not when you want it.
You cannot pass arguments or guarantee ordering, and it should only touch unmanaged state.
Dispose:
Deterministic: called explicitly or by a using block, so resources free immediately.
Can clean up both managed and unmanaged resources.
Why avoid finalizers:
Finalizable objects survive an extra GC generation (promoted, then collected later), increasing memory pressure.
Non-deterministic timing means resources stay open longer than necessary.
If you do need one (rare, only for raw unmanaged handles), call GC.SuppressFinalize(this) in Dispose so a cleaned-up object skips finalization. Better still, wrap the handle in a SafeHandle and avoid the finalizer entirely.
Q63.Can you explain the IDisposable pattern, when you would implement it, and how it differs from the GC's role?
IDisposable pattern, when you would implement it, and how it differs from the GC's role?The IDisposable pattern provides deterministic release of resources the GC doesn't manage (file handles, sockets, DB connections) via a Dispose() method. The GC reclaims managed memory automatically but knows nothing about these external resources, so you implement IDisposable to free them promptly.
When to implement it: Your type holds unmanaged resources directly, or owns other IDisposable fields it must release.
How it differs from the GC:
GC: non-deterministic, manages only managed heap memory, runs when it decides.
Dispose: deterministic, you decide when, covers resources the GC can't see.
The canonical pattern:
A protected Dispose(bool disposing) separates managed cleanup (when disposing is true) from unmanaged cleanup (always).
Public Dispose() calls it with true and then GC.SuppressFinalize(this).
Q64.What is the difference between IDisposable and IAsyncDisposable, and when is the latter required?
IDisposable and IAsyncDisposable, and when is the latter required?Both release resources deterministically, but IDisposable.Dispose() is synchronous while IAsyncDisposable.DisposeAsync() returns a ValueTask so cleanup can await I/O. Use the async version when releasing a resource itself involves asynchronous work.
IDisposable: Synchronous Dispose(); consumed with using.
IAsyncDisposable:
Async DisposeAsync(); consumed with await using.
Needed when teardown does real async work: flushing a buffered network stream, committing/closing an async DB connection, draining a Channel.
Why it matters:
Doing async cleanup synchronously (e.g. .GetAwaiter().GetResult()) risks deadlocks and blocks threads.
A type may implement both; prefer await using when available.
Q65.How do you properly dispose of unmanaged resources in a managed environment?
You wrap unmanaged resources (OS handles, native memory) so they are released deterministically and reliably, even on exceptions. The modern, safest approach is to encapsulate the raw handle in a SafeHandle and expose IDisposable, rather than managing an IntPtr with a finalizer by hand.
Preferred: SafeHandle: It owns the handle, releases it in its own finalizer, and is robust against handle-recycling and async exceptions.
If wrapping a raw handle directly, use the full Dispose pattern:
Implement Dispose() for deterministic release and a finalizer (~Type) as a backstop if the caller forgets.
Call GC.SuppressFinalize(this) in Dispose to avoid the finalizer running needlessly.
Free unmanaged state in both paths; only touch managed objects when disposing is true.
Let the consumer release deterministically: Wrap usage in using so cleanup happens even if an exception is thrown.
Q66.Explain the difference between Just-In-Time (JIT) compilation and Native AOT (Ahead-of-Time). When would you prefer one over the other?
JIT) compilation and Native AOT (Ahead-of-Time). When would you prefer one over the other?JIT compiles IL to native code at runtime on first use, while Native AOT compiles everything to a native binary at build time so there is no IL or JIT at run time. JIT adapts to the actual machine and workload; AOT trades that adaptability for fast startup and a self-contained executable.
JIT (Just-In-Time):
Ships IL plus the runtime; methods are compiled lazily as they execute.
Can optimize for the exact CPU and adapt via tiered compilation and Dynamic PGO.
Cost: slower startup and JIT warm-up, plus a larger runtime footprint.
Native AOT:
Produces a single self-contained native binary with no JIT and no IL at run time.
Very fast startup, low memory, small deploy; no runtime install needed.
Cost: requires trimming, and breaks reflection-heavy or runtime code-gen scenarios.
When to prefer each:
Choose JIT for long-running servers, plugin/reflection-heavy apps, and peak throughput.
Choose AOT for CLI tools, serverless/containers, and anything where cold-start and size matter.
Q67.What is the purpose of the deps.json and runtimeconfig.json files generated during a .NET build?
deps.json and runtimeconfig.json files generated during a .NET build?Both are JSON files emitted next to the app to tell the host how to run it. deps.json describes the app's dependencies and assets, while runtimeconfig.json describes which runtime to use and how to configure it.
deps.json:
Lists assemblies, NuGet packages, and native libraries the app depends on, with their paths and versions.
Used by the host to build the assembly probing/resolution graph at load time.
runtimeconfig.json:
Specifies the target framework and runtime version (tfm, framework).
Holds runtime knobs like GC mode, thread pool settings, and configProperties.
Together they let the same build run portably without hard-coding paths or runtime choices into the executable.
Q68.Explain the difference between 'Self-contained' and 'Framework-dependent' deployment models.
The difference is whether the .NET runtime ships with your app. Framework-dependent deployments rely on a shared runtime installed on the machine, while self-contained deployments bundle the runtime so the app runs anywhere without a prior install.
Framework-dependent (FDD):
Ships only your code; uses a globally installed .NET runtime of a compatible version.
Small deploy size and shared servicing/patching of the runtime.
Risk: fails if no compatible runtime is present on the target machine.
Self-contained (SCD):
Bundles the runtime and all dependencies; runs with no .NET installed.
Larger output and per-RID builds; you own runtime patching.
Predictable: the exact runtime version you tested ships with the app.
Related options: Both can pair with single-file publishing; self-contained can add trimming to reduce size.
Q69.What are Span<T> and Memory<T>, and how do they help reduce allocations?
Span<T> and Memory<T>, and how do they help reduce allocations?Both are type-safe views over a contiguous region of memory (array, string, stack, or unmanaged memory) that let you slice without copying. Span<T> is a stack-only struct for synchronous hot paths; Memory<T> is a heap-friendly equivalent you can store and use across await.
They represent a window, not a copy:
Slicing with Slice() or indexers gives a new view over the same backing memory: no new allocation.
This replaces patterns like Substring or Array.Copy that would otherwise allocate.
Span<T>: fast but constrained: A ref struct living only on the stack, so it can wrap stack memory (stackalloc) safely.
Memory<T>: storable: Can be a class field, captured in a closure, and passed through async methods; call .Span to get a Span<T> when you actually touch the data.
Net effect: Parsing, slicing, and buffer processing become allocation-free, cutting GC pressure in high-throughput code.
Q70.What is Span<T> and how does it allow for high-performance, allocation-free memory access?
Span<T> and how does it allow for high-performance, allocation-free memory access?Span<T> is a lightweight ref struct that represents a contiguous block of memory as a pointer plus a length. It lets you read and slice arrays, strings, and stack or unmanaged memory uniformly without copying or allocating.
It is just a pointer + length: No backing object of its own: it borrows existing memory, so creating or slicing one allocates nothing on the heap.
Unifies many memory sources: The same Span<T> API works over a T[], a stackalloc buffer, a string (as ReadOnlySpan<char>), or native memory.
Slicing replaces allocation: span.Slice(start, length) gives a sub-window in O(1) with no copy, replacing Substring-style allocations in parsing.
Stack-only for safety and speed: Being a ref struct it can never escape to the heap, so the runtime guarantees the borrowed memory stays valid and bounds checks can often be elided.
Q71.What is the contract between Equals and GetHashCode, and why must they be consistent?
Equals and GetHashCode, and why must they be consistent?The contract is: if two objects are equal by Equals, they must return the same GetHashCode. This consistency is what hash-based collections rely on to find items correctly.
The core rule: Equal objects must have equal hash codes. The reverse need not hold: unequal objects may share a hash code (a collision).
Why it matters: Dictionary and HashSet first bucket by hash code, then compare with Equals. A wrong hash sends a lookup to the wrong bucket, so equal items are never found.
Other requirements:
Hash code must be stable while the object is in the collection, so base it only on immutable fields.
If you override Equals, always override GetHashCode too.
Q72.What is the difference between reference equality and value equality, and how do == and Equals differ?
== and Equals differ?Reference equality asks whether two variables point to the same object in memory; value equality asks whether two objects are equivalent in content. == and Equals can mean either, depending on how the type defines them.
Reference equality: Same identity (same heap object). Object.ReferenceEquals(a, b) always tests this and cannot be overridden.
Value equality: Equal content even if distinct objects, e.g. two strings with the same characters.
How == behaves:
For most reference types it defaults to reference equality, but types can overload it (string overloads it to compare content).
It is resolved at compile time (static), so it depends on the variable's declared type.
How Equals behaves: A virtual method resolved at runtime; overridden to express value equality (as string and records do).
Value types: struct comparisons are value-based by default, and record types generate value equality for you.
Q73.How does the finally block work, and what are exception filters in .NET?
finally block work, and what are exception filters in .NET?A finally block runs whether or not an exception is thrown, making it the place for guaranteed cleanup; exception filters (when) let a catch decide whether to handle an exception without unwinding the stack first.
finally semantics:
Executes after try/catch on success, on a handled exception, and even on an unhandled one as it propagates.
Used for releasing resources (closing files, connections); using is syntactic sugar over try/finally.
Edge case: it can be skipped by abrupt process termination (e.g. Environment.FailFast or a power loss).
Exception filters:
catch (Ex e) when (condition) only enters the block if the boolean is true; otherwise the search continues.
Key benefit: when the filter is false the stack is not unwound, so the original throw context is preserved for debugging.
Useful for logging (a filter that logs and returns false) or conditional handling (e.g. by HTTP status code).
Q74.Explain the difference between CoreCLR and CoreFX. How do they relate to the modern .NET (5+) architecture?
CoreCLR and CoreFX. How do they relate to the modern .NET (5+) architecture?CoreCLR is the runtime (execution engine) and CoreFX was the set of foundational base-class libraries; together they formed .NET Core, which evolved into the unified modern .NET (5+) platform.
CoreCLR = the runtime:
Includes the JIT compiler (RyuJIT), the garbage collector, type loading, and exception handling.
Executes the IL produced by the compiler.
CoreFX = the libraries:
The implementation of the base class library: collections, System.IO, System.Linq, networking, etc.
Surfaced to developers largely through the System.* namespaces and .NET Standard.
Relationship to modern .NET (5+):
.NET 5 unified .NET Core, Mono/Xamarin, and the old framework into one product line; the CoreCLR + CoreFX split is now an internal/historical distinction.
The repos were consolidated (the runtime and libraries live together in dotnet/runtime), so you just target a single net8.0-style TFM.
Q75.What is a ref struct (like Span<T>) and why can it only exist on the stack?
ref struct (like Span<T>) and why can it only exist on the stack?A ref struct is a value type the compiler guarantees will only ever live on the stack, never on the heap. Span<T> uses this to safely point into stack, native, or managed memory without the reference being captured somewhere that could outlive it.
Why stack-only:
A ref struct can hold a managed reference into the middle of an array or into stackalloc memory; if it escaped to the heap, that reference could dangle or break the GC.
Stack-only lifetime ties it to the current method frame, keeping the pointer valid.
Compiler-enforced restrictions:
Cannot be a field of a class or a normal struct, cannot be boxed, and cannot be captured by a lambda or async state machine.
Cannot be used as a generic type argument or implement interfaces (which would box it).
Cannot be used across an await or yield boundary, since those may move state to the heap.
Payoff: Enables zero-allocation, slicing views over memory (Span<T>, ReadOnlySpan<T>) with full GC safety.
Q76.What is the Pinned Object Heap (POH), and when would the runtime use it?
POH), and when would the runtime use it?The Pinned Object Heap (POH) is a separate managed heap introduced in .NET 5 for objects that must stay at a fixed memory address, letting the GC keep them out of the normal heap so they don't block compaction.
The problem it solves: Pinned objects (via GCHandle or fixed) cannot be moved, so when scattered through the normal heap they create holes that prevent compaction and cause fragmentation.
How POH helps: All pinned objects live together on the POH, so the rest of the heap compacts freely and fragmentation is isolated to one region.
When the runtime uses it: Mainly for interop and high-performance I/O: buffers passed to native code, pinned byte[] buffers for sockets/files that the OS writes into directly.
How to allocate on it: Use GC.AllocateArray<T>(length, pinned: true); the object is pinned for its lifetime without a separate handle.
Q77.How do you handle or prevent Large Object Heap fragmentation?
LOH fragmentation happens because the Large Object Heap (objects ≥ 85 KB) is normally swept but not compacted, leaving gaps between freed objects; you prevent it mainly by allocating large objects less often and reusing them.
Why it fragments: The LOH skips compaction by default (moving big objects is costly), so freed regions leave holes new allocations may not fit into.
Pool and reuse buffers: Use ArrayPool<T> or your own pool so large arrays are reused instead of repeatedly allocated and freed.
Avoid large allocations: Stream data in chunks, use Span<T>/slicing, and avoid growing huge collections that repeatedly reallocate.
Use a consistent buffer size: Fixed-size large buffers fit back into the holes left by prior ones, reducing fragmentation.
Force LOH compaction (last resort): Set GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce then collect; it's expensive, so use sparingly.
Q78.How does the .NET ThreadPool's 'Hill Climbing' algorithm work?
ThreadPool's 'Hill Climbing' algorithm work?Hill Climbing is the feedback control algorithm the .NET ThreadPool uses to decide how many worker threads to run: it nudges the thread count up or down and keeps changes that improve throughput, searching for the count that maximizes completed work.
The goal: Find the thread count that maximizes task completion rate without over-subscribing the CPU (too many threads cause context-switching overhead).
How it adapts:
It periodically changes the thread count slightly, measures throughput, and moves in the direction that improved it: like climbing toward a peak.
It deliberately injects small variations and uses signal-processing techniques to distinguish real improvement from noise.
Interaction with starvation handling: Separately, if the queue isn't draining, the pool injects threads gradually (roughly one per ~500 ms), which is why blocking many pool threads causes slow ramp-up and starvation.
Practical implication: Don't block ThreadPool threads with synchronous waits; use async/await so the pool isn't fighting its own algorithm.
Q79.How does the .NET Garbage Collector decide when to promote an object from Generation 0 to Generation 1?
An object is promoted from Gen0 to Gen1 simply by surviving a Gen0 collection: if it's still reachable when the GC runs, it moves up a generation rather than being reclaimed.
Survival, not age in time: Promotion is event-driven by collections, not by elapsed time: surviving one Gen0 GC promotes you to Gen1, surviving a Gen1 GC promotes you to Gen2.
Why it works this way: The generational hypothesis assumes most objects die young; one that already survived is statistically likely to live longer, so it's worth checking less often.
When a Gen0 collection triggers: When the Gen0 budget (a dynamically tuned allocation threshold) is exhausted, the GC collects Gen0 and promotes survivors.
Consequence: Objects with medium lifetimes that survive Gen0 but die soon after ("mid-life crisis") get promoted and then linger in Gen1/Gen2, which is costly to collect.
Q80.What is the 'Zero Garbage Collector' (GC) and in what specific scenarios would a developer use it?
GC) and in what specific scenarios would a developer use it?The Zero (or "null") GC is a no-op garbage collector: it allocates memory but never reclaims it, used in short-lived or latency-critical processes where you want allocation speed and zero GC pauses and don't care about freeing memory.
What it does: It satisfies allocations from a fixed region but never collects; when memory runs out, the process simply stops/crashes.
Why use it: Eliminates all GC pause overhead and the cost of tracking objects, giving the most deterministic latency possible.
Where it fits:
Very short-lived processes (CLI tools, serverless functions, batch jobs) that finish before exhausting memory, so cleanup is irrelevant.
Benchmarking and diagnosing whether GC is your bottleneck.
How it's enabled: It's a pluggable "standalone GC" loaded via the DOTNET_GCName environment variable pointing to the collector library; it's a niche, advanced feature, not a normal setting.
The tradeoff: Unbounded memory growth: never use it for long-running services.
Q81.What is the difference between Workstation GC and Server GC, and when would you choose each?
Both are flavors of the .NET garbage collector tuned for different workloads: Workstation GC optimizes for low latency and responsiveness on a single client machine, while Server GC optimizes for throughput on multi-core servers by using parallel collection across multiple heaps.
Workstation GC:
Default for client/desktop apps; uses a single managed heap and one GC thread (per logical heap).
Minimizes pauses to keep UI responsive; lower memory footprint.
Server GC:
Default for ASP.NET Core and server workloads; creates a separate heap and dedicated GC thread per logical processor.
Collections run in parallel, maximizing throughput but using more memory and CPU.
How to choose:
Use Server GC for high-throughput, multi-core server apps with plenty of RAM.
Use Workstation GC for desktop/client apps or low-memory / many-instance scenarios (e.g. dense container hosting).
Configurable via <ServerGarbageCollection> in the project file or runtimeconfig.
Q82.What is Background (concurrent) Garbage Collection and how does it reduce application pause times?
Background GC lets the garbage collector reclaim full (gen 2) heaps on a dedicated background thread while application threads keep running, so long generation-2 collections no longer require fully stopping the app.
The problem it solves: Gen 0 and gen 1 collections are quick, but a full gen 2 collection can be long; doing it as a blocking pause hurts latency.
How it works:
A separate background GC thread performs the gen 2 collection concurrently with running application code.
Short gen 0/gen 1 (ephemeral) collections can still occur during the background collection, keeping allocations satisfied.
Why pauses shrink: Only brief stop-the-world moments are needed to synchronize, instead of one long blocking gen 2 pause.
Availability: Enabled by default and works with both Workstation and Server GC (replacing the older non-concurrent mode).
Q83.What is the finalization queue (and freachable queue), and how do finalizable objects affect GC?
Q84.Explain how the async and await state machine works under the hood.
async and await state machine works under the hood.Q85.What does ConfigureAwait(false) do, and why is it less critical in ASP.NET Core compared to legacy .NET Framework?
ConfigureAwait(false) do, and why is it less critical in ASP.NET Core compared to legacy .NET Framework?Q86.What is a deadlock in an async context, and how can sync-over-async cause it?
async context, and how can sync-over-async cause it?Q87.What is the purpose of a SynchronizationContext, and how has its importance changed in .NET Core/5+ compared to the legacy .NET Framework?
SynchronizationContext, and how has its importance changed in .NET Core/5+ compared to the legacy .NET Framework?Q88.What is the difference between Mutex, Semaphore, SemaphoreSlim, and Monitor for synchronization?
Mutex, Semaphore, SemaphoreSlim, and Monitor for synchronization?Q89.What does the volatile keyword do, and what problem does the .NET memory model address with memory barriers?
volatile keyword do, and what problem does the .NET memory model address with memory barriers?Q90.What is TaskCompletionSource and when would you use it to bridge non-Task-based APIs?
TaskCompletionSource and when would you use it to bridge non-Task-based APIs?Q91.What is a 'Captive Dependency'? How can you detect and prevent it in a .NET application?
.NET application?Q92.Why is it generally recommended to inject IServiceScopeFactory instead of IServiceProvider when you need to resolve services manually?
IServiceScopeFactory instead of IServiceProvider when you need to resolve services manually?Q93.What is an AssemblyLoadContext, and how does it replace the legacy AppDomain for isolation?
AssemblyLoadContext, and how does it replace the legacy AppDomain for isolation?Q94.How does the CLR handle 'Strong Naming,' and is it still relevant in modern cross-platform .NET?
CLR handle 'Strong Naming,' and is it still relevant in modern cross-platform .NET?Q95.How do Source Generators differ from Reflection? Why is the industry moving toward Source Generators for performance-critical applications?
Q96.How are Generics implemented in the .NET runtime (reification) compared to Java (type erasure)?
Q97.What is covariance and contravariance in .NET generics, and where do you see them in the BCL?
BCL?Q98.What is a WeakReference, and in what scenarios would you use one?
WeakReference, and in what scenarios would you use one?Q99.What is a SafeHandle and why is it preferred over a raw IntPtr for wrapping unmanaged resources?
SafeHandle and why is it preferred over a raw IntPtr for wrapping unmanaged resources?Q100.What is 'Tiered Compilation' and how do Tier 0 and Tier 1 optimizations work at runtime?
Tier 0 and Tier 1 optimizations work at runtime?Q101.Explain 'ReadyToRun' (R2R) and how it differs from a fully JIT-compiled or fully AOT-compiled application.
ReadyToRun' (R2R) and how it differs from a fully JIT-compiled or fully AOT-compiled application.Q102.What are the trade-offs of using Native AOT? When would you choose it over the standard JIT runtime?
Q103.What is 'Dynamic PGO' (Profile-Guided Optimization), and how does it improve .NET performance?
Q104.Explain the concept of 'Trimming' in .NET. Why is it necessary for AOT, and what are the risks of using it?
Q105.Explain the difference between Span<T> and Memory<T>. Why can't Span<T> be used in asynchronous methods?
Span<T> and Memory<T>. Why can't Span<T> be used in asynchronous methods?Q106.Explain the purpose of SearchValues<T>. How does it use SIMD to optimize performance?
SearchValues<T>. How does it use SIMD to optimize performance?Q107.What is ArrayPool<T> and how does pooling help reduce GC pressure?
ArrayPool<T> and how does pooling help reduce GC pressure?Q108.What is stackalloc and how does it relate to Span<T> for stack-based allocation?
stackalloc and how does it relate to Span<T> for stack-based allocation?Q109.How would you diagnose a CPU spike or a memory leak in a live .NET production environment, and which tools would you use?
Q110.What is P/Invoke, and how does marshaling work when calling unmanaged code from managed .NET?
P/Invoke, and how does marshaling work when calling unmanaged code from managed .NET?