Skip to main content

Command Palette

Search for a command to run...

C# Collections and Generics Interview Questions for Senior .NET Developers (2026)

Updated
โ€ข15 min read
C# Collections and Generics Interview Questions for Senior .NET Developers (2026)

Senior .NET developers are expected to go beyond knowing which collection type exists โ€” interviewers want to understand whether you can reason about trade-offs under pressure: thread safety, allocation patterns, time complexity at scale, and how the C# type system shapes the decisions you make in production code. Collections and generics questions show up in virtually every senior interview, and the answers that separate a mid-level candidate from a senior one are never about memorizing APIs โ€” they're about knowing why.

If you want to go deeper on these patterns with annotated, production-ready code that shows how collections and generics are used inside a real ASP.NET Core API, Patreon has the full implementation โ€” designed around the kind of code enterprise teams actually ship.


Basic: Core Collection Types

What is the difference between Array, List<T>, and LinkedList<T> in C#?

An Array has a fixed size allocated at creation time; accessing any element is O(1) by index, and memory is contiguous, making it highly cache-friendly. List<T> wraps a dynamically resizing array internally โ€” indexed access is O(1), but insertions or deletions at arbitrary positions are O(n) due to the shift. LinkedList<T> uses node-based heap allocation, making insertion and removal at any position O(1) once you have a reference to the node โ€” but indexed access is O(n), and memory fragmentation is a real concern at scale.

In ASP.NET Core, List<T> dominates general-purpose usage. LinkedList<T> is rarely needed outside of specific queue or LRU cache implementations where you need O(1) arbitrary removal.

What is IEnumerable<T> and why does it matter in API design?

IEnumerable<T> is the base interface for all enumerable sequences. It exposes a single GetEnumerator() method, enabling foreach loops and LINQ operations. Its importance in API design is about the guarantee it provides: callers can iterate the sequence, but they cannot assume it is in memory, random-accessible, or thread-safe.

When a method returns IEnumerable<T>, it signals deferred execution potential โ€” the caller should not assume a materialised list. Returning IEnumerable<T> from repository methods can leave LINQ queries unevaluated, which is a common source of bugs and performance issues in EF Core. Returning IReadOnlyList<T> or IReadOnlyCollection<T> from application layer boundaries makes these guarantees explicit.

What is ICollection<T> vs IList<T> vs IReadOnlyList<T>?

Interface Guarantees Allows Mutation?
ICollection<T> Count + Add/Remove + Contains Yes
IList<T> Indexed access + Insert/RemoveAt Yes
IReadOnlyList<T> Indexed access + Count No (read-only)
IReadOnlyCollection<T> Count only No (read-only)

For outbound DTOs and query results, returning IReadOnlyList<T> communicates intent and prevents callers from accidentally casting to List<T> and modifying the underlying collection. Senior candidates should be able to articulate this boundary without prompting.

What is the difference between Dictionary<TKey, TValue> and HashSet<T>?

Dictionary<TKey, TValue> maps keys to values; HashSet<T> stores only unique values with no associated data. Both use hash-based storage โ€” average O(1) for Add, Contains, and Remove. Use HashSet<T> when you only care about membership or uniqueness, without needing an associated value. In high-frequency queries (e.g., checking whether a feature flag key exists), HashSet<T> is preferred over List<T> for O(1) Contains versus O(n).


Intermediate: Generics and Type System

What are C# generics and why do they matter for performance?

Generics allow you to write type-safe, reusable code without boxing. Before generics (ArrayList era), value types stored in collections were boxed into heap objects, adding GC pressure and allocation overhead. With List<T>, an int stays on the stack โ€” no boxing occurs.

For senior developers, the implication is measurable: in hot paths processing large collections of structs (e.g., sensor readings, financial tick data, event payloads), using generic collections instead of object-based ones can cut allocations dramatically. This ties directly to Span<T> and Memory<T> patterns for zero-allocation processing.

What are generic constraints in C# and when do you use them?

Generic constraints restrict what types a type parameter accepts. Common constraints include:

  • where T : class โ€” reference types only

  • where T : struct โ€” value types only, also prevents nullable

  • where T : new() โ€” must have a parameterless constructor

  • where T : IComparable<T> โ€” must implement an interface

  • where T : BaseClass โ€” must inherit from a specific class

  • where T : notnull (C# 8+) โ€” non-nullable reference or value type

In domain-layer generic repositories (IRepository<T> where T : Entity), constraints enforce that only domain entities participate in persistence operations โ€” a design contract enforced at compile time rather than through runtime checks.

What is the difference between covariance and contravariance in C# generics?

Covariance (out T) allows a type to be substituted with a more derived type. IEnumerable<string> is assignable to IEnumerable<object> because IEnumerable<T> declares out T.

Contravariance (in T) allows substitution of a less derived type. IComparer<object> can be used where IComparer<string> is expected.

This matters in generic pipeline designs โ€” a senior engineer writing a generic handler or processor needs to understand when a covariant or contravariant interface enables polymorphism without losing type safety, and when it breaks (generic interfaces are invariant by default, which surprises developers who expect List<string> to be substitutable for List<object>).

What is the difference between IEqualityComparer<T> and IComparable<T>?

IEqualityComparer<T> defines equality and hash codes โ€” used by Dictionary<TKey, TValue> and HashSet<T> to resolve bucket placement and key matching. IComparable<T> defines ordering โ€” used by SortedDictionary<TKey, TValue>, SortedSet<T>, and LINQ OrderBy.

A common interview scenario: you have a custom entity class and need to use it as a dictionary key. You either override GetHashCode() and Equals() on the class itself, or provide a custom IEqualityComparer<T>. Senior candidates know that failing to override GetHashCode() consistently with Equals() leads to silent data corruption in dictionaries โ€” elements are inserted but never found, or duplicates silently accumulate.


Intermediate-Advanced: Thread Safety and Concurrent Collections

What thread-safe collection types does .NET provide and when do you use each?

For deeper coverage of how thread safety intersects with async patterns in high-concurrency APIs, the C# Multithreading and Concurrency Interview Questions article covers synchronisation primitives, Task scheduling, and concurrent access patterns in detail.

Type Thread-Safety Mechanism When to Use
ConcurrentDictionary<TKey, TValue> Fine-grained locking per bucket Shared read-write cache
ConcurrentQueue<T> Lock-free, two-pointer algorithm Producer-consumer pipelines
ConcurrentBag<T> Per-thread storage + stealing Work queues where order doesn't matter
ConcurrentStack<T> Lock-free linked list LIFO work distribution
BlockingCollection<T> Wraps concurrent collections + blocking Bounded producer-consumer with backpressure

ConcurrentDictionary is the most commonly used in ASP.NET Core services โ€” for example, in-memory caches, rate limiter state, or client connection registries. Candidates should know that AddOrUpdate and GetOrAdd are individually atomic but not transactional together โ€” a read-modify-write on two keys is not atomic.

What is the difference between ConcurrentDictionary<K, V> and using Dictionary<K, V> with lock?

ConcurrentDictionary uses striped locking โ€” the dictionary is internally divided into segments, and a lock only blocks operations on that segment. This allows multiple threads to write to different segments simultaneously. A global lock on a Dictionary serialises all reads and writes.

For read-heavy workloads, ConcurrentDictionary provides significantly better throughput. For write-heavy workloads with complex multi-step operations (where you need to atomically update multiple keys), a single lock gives you a cleaner atomicity boundary. ConcurrentDictionary does not give you cross-key atomicity.

In ASP.NET Core, ConcurrentDictionary is the correct default for caches and registries that are accessed concurrently. Using a plain Dictionary with lock is acceptable if the dictionary is small, contains complex multi-key operations, or if you need the simplicity for testability.

What are ImmutableList<T> and FrozenDictionary<TKey, TValue>, and when are they relevant?

ImmutableList<T> (from System.Collections.Immutable) creates a new collection on every modification. It trades mutability for thread safety โ€” because immutable collections never change, they are inherently safe to share across threads with no locking. The trade-off is allocation cost on modification, making them unsuitable for high-mutation paths but excellent for publishing snapshots of read-mostly shared state.

FrozenDictionary<TKey, TValue> (.NET 8+) is optimised for read-heavy, write-never scenarios (e.g., configuration lookup tables, route maps, feature flag registries built at startup). It uses a static, highly cache-friendly internal structure that outperforms Dictionary<K, V> on reads. The cost is that construction is expensive and the dictionary cannot be modified after creation.

Senior engineers should reach for FrozenDictionary when a lookup table is built once at startup and read millions of times โ€” this is exactly the pattern used in IOptions<T> configuration caching and compiled route tables.


Advanced: Performance, Memory, and Design

What is the impact of collection choice on GC pressure in ASP.NET Core?

Every collection allocated on the heap per request contributes to GC pressure. List<T> starts with a default capacity of 4 and doubles on resize โ€” if you know the count up front, initialising with new List<T>(expectedCount) avoids repeated reallocation and array copying.

For collections of value types (e.g., List<int>, List<Guid>), the array is a contiguous block of value memory โ€” efficient for CPU cache. For List<MyClass>, the array stores references โ€” each element access follows a pointer to a heap object.

In hot paths (e.g., per-request result building, batch processing), replacing List<T> with Span<T> or renting arrays from ArrayPool<T>.Shared eliminates per-request allocation entirely. Profiling tools (dotnet-trace, BenchmarkDotNet, PerfView) will surface collection allocations as a top contributor to Gen0 GC pressure in API services under load. For a comprehensive look at the full range of performance concerns senior candidates face, see the ASP.NET Core Performance Optimization Interview Questions guide.

How does Dictionary<TKey, TValue> handle hash collisions, and why does it matter?

Dictionary<TKey, TValue> uses open addressing with chaining internally. When two keys hash to the same bucket, they share a linked list of entries. In the average case with good hash distribution, lookup is O(1). In the worst case with all keys colliding to the same bucket, lookup degrades to O(n).

This matters in production when using custom or poorly implemented GetHashCode() overrides. A GetHashCode() that always returns the same value (a degenerate but valid implementation) turns every dictionary into a linked list under the hood. Senior engineers should know to test collection performance under their actual key distribution, especially for string-keyed dictionaries with domain-specific key patterns.

What is the difference between SortedDictionary<TKey, TValue> and SortedList<TKey, TValue>?

Both maintain sorted key order and provide O(log n) lookup โ€” but their internal structures differ:

  • SortedDictionary uses a red-black tree. Insertions and deletions are O(log n); memory is proportional to the number of elements.

  • SortedList uses two parallel sorted arrays. Lookup is O(log n) via binary search; insertions are O(n) due to array shifting. However, it uses less memory than SortedDictionary and is faster to enumerate.

SortedList is the better choice when your data is loaded once (or infrequently mutated) and iterated frequently โ€” config tables, lookup tables with ordered keys. SortedDictionary is better for data that is frequently inserted or removed while still needing sorted enumeration.

What is Span<T> and how does it relate to collections?

Span<T> is a stack-only reference type that provides a type-safe view over a contiguous region of memory โ€” an array, a stack-allocated buffer, or unmanaged memory โ€” without copying. It is not a collection in the traditional sense; it cannot be stored on the heap or captured in async methods.

In performance-sensitive ASP.NET Core paths (JSON parsing, binary protocol parsing, string slicing), Span<T> and ReadOnlySpan<T> replace string and byte[] allocations. For example, parsing a comma-separated header value by slicing a ReadOnlySpan<char> avoids allocating intermediate string objects for each segment.

Memory<T> is the heap-compatible counterpart, used in async contexts where Span<T> cannot cross an await boundary.


Scenario-Based Questions

You have a service that maintains a shared registry of active WebSocket connections. What collection would you use and why?

The registry needs concurrent read and write access (connections join and leave while other threads enumerate the active set). ConcurrentDictionary<string, WebSocketConnection> is the natural fit โ€” connection ID as key, connection object as value. Add is TryAdd, remove is TryRemove.

If enumeration over all connections needs to be consistent (no torn reads), you would take a snapshot via dictionary.Values.ToList() before iterating โ€” ConcurrentDictionary does not guarantee a stable snapshot during enumeration. If the registry is read far more often than written (e.g., broadcast scenarios), a ReaderWriterLockSlim with a plain Dictionary may offer better throughput at the cost of more complex locking code.

How would you implement a thread-safe in-memory LRU cache using .NET collections?

An LRU cache needs O(1) lookup (eviction check) and O(1) reordering on access. The canonical approach combines a Dictionary<TKey, LinkedListNode<(TKey, TValue)>> for O(1) lookup with a LinkedList<(TKey, TValue)> for ordering โ€” the most recently accessed item moves to the front, and the tail is evicted when capacity is reached.

Thread safety requires a lock around both the dictionary and the list operations together, since they must stay in sync. ConcurrentDictionary alone is insufficient here because it cannot atomically coordinate with the linked list. In .NET 9+, HybridCache provides a production-ready L1/L2 layered cache with stampede protection, which should be preferred over a hand-rolled LRU for most production use cases.

A colleague stores a mutable List<T> in a static field used across requests. What are the risks and how would you fix it?

Storing a mutable collection in a static field that is shared across requests creates race conditions under concurrent access โ€” simultaneous Add, Remove, or Count+index operations without synchronisation will cause data corruption or IndexOutOfRangeException in production.

The fix depends on the read/write pattern. For a configuration-style registry (written once, read constantly): replace with FrozenDictionary or ImmutableList built at startup. For a live registry updated at runtime: use ConcurrentDictionary or protect access with ReaderWriterLockSlim. For simple counters or flags: use Interlocked operations on primitive types rather than collections.


FAQ

What is the time complexity of List<T>.Contains()? O(n) โ€” it performs a linear scan. If you need frequent membership checks, convert to a HashSet<T> for O(1) average lookup. This is a common performance mistake in loops: checking if (list.Contains(item)) inside a loop over thousands of items turns O(n) into O(nยฒ).

Can Dictionary<TKey, TValue> be safely iterated while being modified? No โ€” modifying a dictionary during enumeration throws InvalidOperationException: Collection was modified; enumeration operation may not execute. To iterate and modify, take a snapshot of the keys first (var keys = dict.Keys.ToList()), then iterate the snapshot while modifying the dictionary.

What is the difference between Array.Sort() and List<T>.Sort()? Both use an introspective sort (introsort โ€” hybrid of quicksort, heapsort, and insertion sort) with O(n log n) average complexity. Array.Sort() sorts in place on the underlying array. List<T>.Sort() delegates to the internal array sort. For custom types, both accept an IComparer<T> or Comparison<T> delegate. Span<T>.Sort() is available in .NET 5+ for zero-allocation sorting of spans.

What is CollectionsMarshal.GetValueRefOrAddDefault() and when would a senior engineer use it? It is a performance API in System.Runtime.InteropServices that returns a reference to the value in a Dictionary<TKey, TValue> โ€” or creates and returns a reference if the key is absent. It avoids a double lookup when you need to check existence and then modify the value. This is relevant in hot paths (e.g., frequency counters, aggregation loops) where the overhead of two dictionary lookups (TryGetValue + array index) is measurable at scale.

Why should List<T> be initialised with an expected capacity?List<T> starts with a capacity of 4 and doubles on overflow (4 โ†’ 8 โ†’ 16 โ†’ 32...). Each doubling allocates a new array and copies all elements. If you know the expected count at call time, new List<T>(expectedCount) avoids all intermediate allocations and copies. This is relevant in data pipeline stages where result sets have predictable sizes โ€” pagination results, batch reads, LINQ materialisation.

What is the difference between IEnumerable<T> and IAsyncEnumerable<T>?IEnumerable<T> is synchronous โ€” it blocks the thread while producing each element. IAsyncEnumerable<T> is asynchronous โ€” it supports await foreach, allowing each element to be awaited individually. This is the correct return type for streaming database results, paginated API responses, or any producer that involves I/O. Returning IReadOnlyList<T> materialises all results into memory before the caller sees any; returning IAsyncEnumerable<T> streams them one at a time, reducing peak memory usage under load.

When should you choose ConcurrentBag<T> over ConcurrentQueue<T>?ConcurrentQueue<T> is FIFO and lock-free โ€” use it when processing order matters (task pipelines, request queues). ConcurrentBag<T> has no ordering guarantee and uses per-thread storage with work-stealing โ€” it is optimised for producer-consumer patterns where the same thread that adds items also consumes them, minimising cross-thread contention. ConcurrentBag<T> is rarely the right choice for most ASP.NET Core scenarios; ConcurrentQueue<T> or System.Threading.Channels.Channel<T> are preferred for general-purpose async pipelines.

More from this blog

C

Coding Droplets

270 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.