Top C# Interview Questions for Experienced Developers

Advertisement

Top C# Interview Questions and Answers for Experienced Developers

Preparing for a senior or lead C# interview means going beyond syntax and diving deep into runtime behavior, performance tuning, asynchronous programming, memory management, and modern language features. This long-form guide compiles the top C# interview questions for experienced developers and provides practical, concise answers that demonstrate architectural awareness and production-grade judgment.

Senior C# developer explaining system design on a whiteboard

 

 

Foundations and Runtime

1) What is the CLR, and how do JIT and AOT compilation affect performance?

The Common Language Runtime (CLR) executes managed code, handling memory management, type safety, security, and exceptions. Traditionally, C# code is compiled to IL and JIT-compiled to machine code at runtime. JIT enables tiered compilation and runtime optimizations (like inlining based on profiling), but incurs warm-up costs. Ahead-of-time (AOT) compilation produces native binaries that reduce startup time and memory by removing the JIT. AOT is strong for microservices and serverless cold starts, while JIT can yield optimal steady-state performance via tiered optimizations. Choosing depends on deployment constraints, startup requirements, and the need for reflection/dynamic features.

 

 

2) How does .NET garbage collection work? Explain generations, LOH, and server vs. workstation GC.

.NET GC is generational and compacting. Objects start in Gen 0, survive to Gen 1, and then Gen 2. Short-lived allocations are cheaper; long-lived ones get collected less frequently. The Large Object Heap (LOH) stores large objects (typically arrays > 85 KB); LOH collections are costlier and historically did not compact frequently, so fragmentation mattered. Server GC uses dedicated GC threads and is tuned for throughput on multi-core servers; Workstation GC is tuned for desktop responsiveness. Reducing allocations, pooling buffers, and avoiding unnecessary large arrays minimizes GC pressure and improves latency.

 

 

3) When should you use a struct instead of a class? What about readonly and ref struct?

Use structs for small, immutable value types that are frequently allocated or passed around, such as coordinates or small numeric aggregates. They avoid heap allocation when stored inline in arrays or as fields, reducing GC pressure. Prefer readonly struct when instances are immutable, which allows defensive copies to be avoided. ref struct (e.g., Span<T>) must remain on the stack and cannot escape to the heap, enabling safe, fast stack-based operations. Avoid large or mutable structs; large structs hurt copy performance.

 

 

4) Explain boxing and unboxing and why they matter for performance.

Boxing converts a value type to an object by allocating it on the heap; unboxing extracts the value. Both incur costs: allocations, GC pressure, and potential cache misses. Boxing often appears in generic contexts without constraints, when using non-generic collections, or with params object. Avoid by using generic collections, adding constraints like where T : struct, and writing overloads for value types in hot paths. Monitoring allocations with profilers or BenchmarkDotNet helps catch hidden boxing.

 

 

Diagram illustrating stack vs heap and boxing/unboxing

 

 

Asynchrony and Concurrency

5) Task vs. ValueTask: when should you choose each?

Task is the default for async APIs; it is allocation-friendly when results are not immediately available. ValueTask is a struct that can avoid allocations when a result is often synchronously available or cached. Use ValueTask when you can prove high synchronous completion rates, you avoid multiple awaits on the same instance, and you understand pooling semantics. Most public APIs should remain Task-based for simplicity; ValueTask is best in high-throughput internals where micro-allocations matter.

 

 

6) What are common async/await pitfalls (deadlocks, ConfigureAwait, async void)?

Deadlocks can occur when blocking on async code (e.g., calling .Result on a Task) under a synchronization context. Always propagate async end-to-end. Use ConfigureAwait(false) in library code to avoid capturing contexts and to improve throughput. Reserve async void for event handlers only; prefer async Task for proper error propagation. Ensure exceptions are observed, and be careful with fire-and-forget tasks—wrap them with logging and cancellation awareness.

 

 

7) How do you implement cooperative cancellation correctly?

Use CancellationToken parameters on async APIs. Check IsCancellationRequested or call ThrowIfCancellationRequested at safe points. Pass tokens to I/O calls that support them. For long-running CPU-bound operations, periodically check the token. When using TaskCompletionSource, set it to canceled with TrySetCanceled(token) to preserve the token. Avoid swallowing OperationCanceledException silently; propagate it to signal cancellation rather than failure.

 

 

8) Which synchronization primitives should you use and when?

Use lock (Monitor) for simple mutual exclusion within a process. SemaphoreSlim limits concurrency (e.g., max N parallel operations). ReaderWriterLockSlim benefits read-heavy scenarios with rare writes. Mutex crosses process boundaries but is heavier. Interlocked is ideal for atomic operations on counters/flags. Prefer async-compatible primitives (SemaphoreSlim) in async code; don’t block threads with synchronous locks around await points.

 

 

9) How do you avoid thread pool starvation and improve throughput?

Avoid blocking calls (.Result, .Wait) in async paths. Use truly asynchronous I/O APIs. Keep synchronous work minimal in async methods. For CPU-bound work, offload to Task.Run strategically or use dedicated queues. Size the thread pool via configuration only when profiling proves it necessary; rely on the runtime’s hill-climbing. Batch work, use bounded channels or pipelines, and limit parallelism with SemaphoreSlim or ParallelOptions.

 

 

Modern Language Features

10) How do records, init-only setters, and required members support immutability?

record and record struct provide built-in value-based equality and with-expressions for non-destructive mutation. init setters allow setting properties during object initialization while keeping them immutable afterward. required members ensure critical properties are set during initialization. These features encourage immutable models, improving testability, correctness, and thread safety.

 

 

11) What is nullable reference types (NRT) and how do you use it effectively?

NRT lets the compiler track potential null references. Annotated code distinguishes between nullable (e.g., string?) and non-nullable (string). Fix warnings by validating inputs, using guard clauses, and annotating APIs accurately. The null-forgiving operator (!) should be rare; prefer actual checks. Embrace annotations in public APIs to communicate intent to callers. Combine with ArgumentNullException.ThrowIfNull for clarity.

 

 

12) Explain pattern matching advances: property, relational, and list patterns.

Modern C# adds powerful patterns. Property patterns match by member values (e.g., Size: > 0). Relational patterns combine comparison operators with logical patterns (and/or/not). List patterns match sequences by shape, such as prefix/suffix matches. Together with switch expressions, they replace complex if/else chains with concise, readable logic while remaining exhaustiveness-friendly.

 

 

13) Describe variance in generics: covariance and contravariance.

Covariance (out) allows using a more derived type than originally specified, commonly in IEnumerable<out T>. Contravariance (in) allows using a more general type, used in IComparer<in T> and delegates. Variance applies to interfaces and delegates, not classes. Use variance for read-only or write-only scenarios; for mutable collections, invariance protects type safety.

 

 

14) What constraints can you place on generics and why?

Constraints specify capabilities: where T : class, struct, notnull, unmanaged, new(), or custom interface/class constraints. They enable more efficient code (e.g., unmanaged for interop or span-based operations) and better API guarantees. In modern C#, static abstract members in interfaces enable generic math by requiring operators on generic types.

 

 

15) How do Span<T> and Memory<T> improve performance? What are their limits?

Span<T> is a stack-only view over contiguous memory (arrays, stackalloc, native memory) that enables slicing without allocations. ReadOnlySpan<T> is the immutable counterpart. Memory<T> is heap-friendly and can be stored in fields and awaited across async boundaries, with ReadOnlyMemory<T> for read-only access. Restrictions: Span is a ref struct, cannot be boxed or captured by lambdas across async/iterator boundaries, and requires careful lifetime management.

 

 

Illustration of slicing an array with Span<T> without allocations

 

 

16) When should you use expression trees, reflection, or source generators?

Reflection is flexible but slower and allocation-heavy. Expression trees provide a structured representation of code, enabling dynamic query providers (like ORMs) to translate expressions. They can be compiled to delegates, but compilation is costly; cache results. Source generators run at compile time to emit code, eliminating runtime reflection cost. Choose generators for repetitive boilerplate and hot-path scenarios; use reflection sparingly and cache aggressively.

 

 

Collections, LINQ, and Data

17) What are common LINQ performance pitfalls?

LINQ uses deferred execution, so enumerating multiple times repeats work. Cache with ToList or ToArray when needed. Avoid OrderBy chains if you can combine criteria. Beware of Select creating intermediate allocations in hot paths; consider Span-based APIs for parsing. When integrating with ORMs, ensure your LINQ is translatable; avoid client evaluation surprises by materializing at the right boundary. Measure allocations in tight loops.

 

 

18) How do you choose between Dictionary, ConcurrentDictionary, and ImmutableDictionary?

Dictionary is fastest in single-threaded or externally synchronized cases. ConcurrentDictionary supports thread-safe concurrent reads and writes with efficient lock partitioning; use AddOrUpdate and GetOrAdd to avoid race conditions. ImmutableDictionary is ideal for highly concurrent read-mostly scenarios where snapshot semantics simplify reasoning; updates allocate new versions but share structure internally.

 

 

19) What’s the difference between List and Array, and when is ArrayPool useful?

Array is fixed-size and contiguous; List resizes dynamically by allocating new arrays. Arrays are slightly faster for known-size collections. For high-throughput scenarios, ArrayPool rents and returns arrays to avoid LOH and frequent allocations, particularly for buffers and serialization. Ensure you clear sensitive data before returning to the pool and always return rented arrays to prevent leaks.

 

 

20) How do you avoid multiple enumeration and iterator invalidation issues?

Multiple enumeration redoes work and can cause side effects if the source changes. Call ToList to materialize once when you need repeated access. Avoid mutating a collection during enumeration; use ToList or ToArray to snapshot before modifications. In high-performance code, prefer span-based loops or manual iteration to control allocations and branches.

 

 

Resource Management and I/O

21) Explain the IDisposable pattern and IAsyncDisposable.

Implement IDisposable to release unmanaged resources deterministically. The recommended pattern uses a protected Dispose(bool disposing), calling GC.SuppressFinalize when disposing is true. For async disposable resources (e.g., streams with async flush), implement IAsyncDisposable and use await using. Prefer SafeHandle over finalizers for native handles. Only add a finalizer when absolutely necessary; finalizers delay GC and complicate lifetime.

 

 

22) How do you implement robust file and network I/O with backpressure?

Use truly async APIs (Stream.ReadAsync/WriteAsync, HttpClient methods) and propagate cancellation. Apply backpressure with bounded channels, pipelines, or SemaphoreSlim to limit concurrency. For high-throughput scenarios, consider System.IO.Pipelines to minimize copies with Span<T>. Add retries with jitter for transient failures and timeouts to avoid resource exhaustion. Always dispose I/O resources deterministically.

 

 

23) What are safe ways to handle large payloads without excessive allocations?

Process data in chunks using streams or pipelines. Use ArrayPool or MemoryPool for temporary buffers. Parse using Span<T> and ReadOnlySequence<T> to avoid copying. Avoid building gigantic strings; use StringBuilder or streaming writers. For JSON, prefer modern high-performance libraries and use reader/writer APIs that work with spans.

 

 

Streaming pipeline showing chunked reading and processing

 

 

Diagnostics, Reliability, and Design

24) What are best practices for exception handling in C#?

Use exceptions for exceptional conditions, not control flow. Throw the most specific exception type and include actionable messages. Avoid catching broad exceptions unless at process boundaries; rethrow with a bare throw to preserve stack. Don’t use exceptions for expected validation; return results or use guard clauses. In async methods, exceptions propagate via Task; ensure they are observed and logged. Map exceptions to appropriate HTTP status codes at boundaries.

 

 

25) How do you instrument and benchmark C# code effectively?

Use BenchmarkDotNet for microbenchmarks; it handles warm-up, outliers, and actual runtime scheduling. For production diagnostics, use EventCounters, dotnet-trace, PerfView, or your APM of choice. Profile allocations and CPU hotspots, not just wall-clock time. Add structured logging with correlation IDs and measure the impact of changes via A/B testing or canary releases. Always validate performance hypotheses with measurements.

 

 

26) How do you mitigate memory leaks in managed code?

Common culprits include long-lived references (static caches), event subscriptions not unsubscribed, and timers/TaskCompletionSource holding references. Use WeakReference or weak event patterns where appropriate. Ensure to dispose timers, HttpClient handlers, and streams. Monitor memory with dotMemory or the built-in diagnostic tools. Regularly review caches for eviction policies and size limits.

 

 

27) What are common pitfalls with events and delegates?

Events can keep subscribers alive if not unsubscribed, causing leaks. Use += and -= symmetrically or lifetime-bound subscriptions. Be mindful of closures capturing large objects in lambdas; capture only what you need. Prefer multicast delegates only when order and exception behavior are acceptable; otherwise, loop over subscribers and handle exceptions per-listener.

 

 

28) How do you implement robust configuration and options in .NET apps?

Bind configuration to typed options classes and validate them at startup. Prefer immutable options records with clear defaults. Watch for changes with monitored options where hot reload is needed, but ensure thread safety. Don’t scatter configuration lookups; inject typed options via DI to centralize configuration.

 

 

29) What’s the right way to compare strings and dates?

Use StringComparison.Ordinal or OrdinalIgnoreCase for identifiers and protocol tokens; use culture-aware comparisons only for user-facing text. Normalize case once when feasible. For dates, store in UTC; convert for display at the edge. Beware of DateTimeKind and prefer DateTimeOffset for time zone-aware operations. Avoid implicit conversions that lose offset context.

 

 

30) How do you design clean, testable C# code using DI?

Program to interfaces, not implementations. Keep classes small, cohesive, and single-responsibility. Inject abstractions for external systems (I/O, time, random, HTTP) to enable deterministic tests. Avoid static global state. Prefer composition over inheritance and avoid deep class hierarchies. Use records and immutable models to reduce hidden side effects.

 

 

Advanced Topics

31) What should you know about P/Invoke and marshalling in C#?

P/Invoke allows calling native libraries. Correct marshalling is essential: strings can be ANSI or UTF-16; structures need sequential or explicit layout; arrays and spans may require pinning. Prefer SafeHandle for native handles. Validate calling conventions and lifetime ownership. Keep interop boundaries minimal and batch calls to reduce transitions.

 

 

32) How can you use unsafe code responsibly?

unsafe enables pointers, stackalloc, and fixed statements for maximum performance. It should be isolated, reviewed, and thoroughly tested. Combine with Span<T> APIs to minimize unsafe regions. Ensure correct bounds checking and lifetime management. Unsafe code can outperform in parsing, SIMD operations, and interop glue, but only when profiled evidence justifies it.

 

 

33) What is tiered compilation and how does it affect performance?

Tiered compilation starts methods with quick-to-JIT code and promotes hot methods to higher-optimized tiers over time. This balances startup time and steady-state speed. Don’t over-tune via attributes unless profiling demands it; the runtime generally makes good decisions. Be aware that warm-up phases may show different performance than steady-state.

 

 

34) How do you leverage SIMD and hardware intrinsics?

Use System.Numerics.Vector for portable SIMD on supported types. For advanced scenarios, hardware intrinsics (e.g., Vector128) unlock platform-specific instructions. Keep code paths fallback-safe, and verify speedups with benchmarks. Align data and operate on contiguous buffers; combine with Span<T> to feed vectorized loops efficiently.

 

 

35) What are source generators and incremental generators good for?

Source generators create code at compile time, eliminating reflection and boilerplate. Incremental generators process inputs efficiently across builds. Use them for DTO mappers, DI registration, serialization metadata, and strongly typed bindings. They can improve startup and runtime performance while keeping codebases DRY and analyzable.

 

 

Developer reviewing analyzer warnings and source-generated files

 

 

Practical Scenarios and Q&A

36) How do you prevent async context capture issues in libraries?

In library code, consistently use ConfigureAwait(false) to avoid resuming on a captured context. This prevents deadlocks in UI or legacy synchronization contexts and improves throughput. In application code with UI or specific threading needs, omit ConfigureAwait to marshal back to the UI context.

 

 

37) How do you design cancellation-aware APIs without leaky abstractions?

Accept a CancellationToken in public async methods and pass it down to callee APIs. Don’t store tokens indefinitely or combine them in ways that obscure ownership. For long pipelines, create linked tokens with scopes that clearly define responsibility. Document whether cancellation is best-effort or strict.

 

 

38) How do you safely cache and reuse HttpClient?

HttpClient should be reused to avoid socket exhaustion. Prefer creating it via IHttpClientFactory to manage handlers, DNS refresh, and resilience policies. Tune timeouts and add retry policies with jitter for transient errors. Dispose response content promptly and stream large payloads to avoid buffering.

 

 

39) What’s the difference between with-expressions on records and copy constructors?

With-expressions provide concise non-destructive mutation while preserving value-based equality semantics of records. They avoid manual copy constructors and reduce boilerplate. With classes, you can emulate via records or implement custom cloning carefully; with records, the compiler generates the necessary methods.

 

 

40) How do you ensure equality and hashing are correct?

For records, equality and GetHashCode are generated based on positional or property members. For classes, implement IEquatable<T> for performance and correctness. Ensure GetHashCode distributes well and is consistent with Equals. For mutable objects, avoid using them as keys or ensure immutability of key fields.

 

 

41) How do you keep allocations low in hot paths?

Use pooled buffers, Span<T>, and stackalloc for small temporary arrays. Reuse value-type enumerators where possible. Avoid LINQ in tight loops; write explicit loops or use simd-friendly algorithms. Cache delegates, pre-size collections, and avoid capturing closures inadvertently. Profile to identify the 20% of code causing 80% of allocations.

 

 

42) When would you choose Channels, TPL Dataflow, or Parallel.ForEach?

Channels provide producer-consumer queues with backpressure and are great for pipelines. TPL Dataflow offers rich blocks for transformations and buffering, good when you need robust graph-like flows. Parallel.ForEach is simple for CPU-bound parallelism over in-memory collections but less flexible for I/O-bound or backpressure scenarios.

 

 

43) How do you diagnose and fix deadlocks?

Capture dumps or stack traces to identify blocking waits. Avoid lock ordering cycles by establishing a strict global order. Don’t mix synchronous locks with async awaits; use SemaphoreSlim for async. Limit lock granularity and keep critical sections minimal. Prefer immutable data and lock-free algorithms where appropriate.

 

 

44) What are recommended practices for logging in high-throughput services?

Use structured logging with minimal allocation. Avoid string concatenation; let the logger render templates. Set appropriate log levels and avoid logging in hot loops. Batch and asynchronously ship logs to outputs. Include correlation IDs and context to enable tracing across microservices.

 

 

Closing Advice

45) If you had to summarize senior-level C# interview essentials, what would they be?

Master the runtime: understand GC, threading, and async. Write APIs that are cancellation-aware, immutable by default, and clearly annotated with nullable reference types. Use modern language features judiciously—records, pattern matching, Span<T>, and ValueTask—only where they add clarity or measurable performance. Measure before optimizing, and validate assumptions with benchmarks and profiling. Keep dependencies clean, code testable, and resource lifetimes explicit.

 

 

Team conducting a technical interview reviewing performance benchmarks

 

 

Conclusion

Experienced C# developers distinguish themselves by combining language mastery with production-oriented judgment. The interview questions above target exactly that: how you reason about asynchrony without deadlocks, pick the right data structures, control allocations, and design APIs that are robust, testable, and efficient. Use these answers to refresh your knowledge and, more importantly, to shape the stories you tell about real systems you have built, tuned, and maintained in .NET.

 

Comments0

Mary Alice
Please write your comment.

Please login to comment.

Menu
Top