← Home
.NETOOP and C# basicsReported by candidates

What are collections in C#? Compare an array, ArrayList and List<T>; what is the difference between IEnumerable, ICollection and IList; what is HashSet, and which collection would you use when?

11 reports in the question index (7 linked here), from screenings and first technical rounds: EPAM, Excellon Software, Wipro, Solsynch and others. It arrives as "collections in C#", then narrows to array vs ArrayList, IEnumerable, or HashSet.

Say this first · about 30 seconds
Make it yours · Navistar Financial end-of-term excess mileage (invented example)
The code, simplified

The matcher, in C#. A nightly job calls FindOpen with the 6,000 open contracts and the 38,000 returned . Tap Next to walk them through the set.

Navistar.sln · where this file sits
  • Mileage/
VinMatcher.cs · line 1 · 1 of 14A class is a reference type: a variable would hold an address to an object. says this class is only a home for methods: you cannot create one with .

Fix later in your repo Example written for this card: invented, not from a real codebase.

Say it out loud, then check

Show the 30-second answer
  • A collection is a group of items your code works through. The choice comes down to four questions: is the size fixed or does it grow, does order matter, must every item be unique, and do you look items up by position, by value or by key.
  • An array is the fastest and smallest, but its length is fixed when it is created. ArrayList is the old list from before generics, when the item type was not written down: it stores every item as an object, so a number gets wrapped in an object and you cast it back on the way out. List<T> is the replacement: you write the item type once in the angle brackets, the compiler checks every use, and nothing is wrapped.
  • The interfaces are about what a caller is allowed to do. The enumerable interface only lets you loop over the items; the collection interface adds a count, add and remove; the list interface adds an indexer, items by position, and an insert method. I return the narrowest one the caller needs, usually a read-only list, so they cannot change my list behind my back.
  • A HashSet is for uniqueness and membership. Add returns false for a duplicate. Contains is fast on average: it turns the value into a hash, a number, and checks one small bucket (a group of items that share a hash) instead of scanning. The catch is that it uses the type's equality rules, so a plain class with no override compares by reference, and changing a field used in the hash after insertion makes the item unfindable.
  • My rule is an array for a fixed buffer, a list for an ordered growable sequence, a set for unique items or fast membership, and a dictionary when each item needs a key. I also check the cost and whether the collection is shared across threads, because the standard list and dictionary are not thread-safe, meaning two threads changing them at once can corrupt them.
Check your answer: tick what you said

You said 0 of 9

Then add your example: Navistar Financial end-of-term excess mileage (invented example)
  • Situation: at end of term, 38,000 Navistar Financial trucks came back with a , the vehicle identification number that uniquely names the truck, and a mileage reading. A nightly job had to find the open contract for each so it could charge the excess miles. The open contracts sat in a of 6,000, and the job scanned the list for every truck. On a test of that size the scan took 0.83 seconds; in production the feed is larger, and scanning once per truck makes the work grow much faster than the data.
  • What I did: I built a of the open once, before the truck loop, and asked the set instead. A set turns the string into a hash, a number, and checks one bucket (a small group that shares a hash), so a miss costs one lookup, not 6,000 string comparisons.
  • Result: the same test then took 6 milliseconds. A test with 10,000 fake checked that the set and the old list found the same contracts, so the speed came without a behaviour change.
  • What I'd say: a is for order; a is for membership and uniqueness. If the question is "is this open?", a list is the wrong tool, and doing it once per truck makes the cost grow with the square of the data (100 trucks against 100 contracts is 10,000 checks).

See it

1/6The report asks the shared cache (data kept in memory) for its contracts: the cache's own List.

In plain words

The senior answer · about 2 minutes
How I choose a collection
  • I ask four questions in order. Is the number of items fixed, is order part of the meaning, must items be unique, and how will the caller find an item: by position, by value, or by a key? The answers pick the type before any code is written.
  • Fixed buffer: an array. Ordered and growing: a . Unique items or a yes/no membership test: a . A value per key: a . Work processed in a special order: a queue or a stack. Threads writing at once: a concurrent collection such as (a thread-safe dictionary), or a .
  • Then I check the cost of the operation the code actually performs. Index access on an array and a list is O(1); a list search is O(n); a set or dictionary lookup is O(1) on average. A scan inside a loop over the same data turns into O(n²), which is the collection bug I see most often in reviews.
Array, ArrayList and List<T>
  • An array is one block of created with a fixed length. The index is the position, starting at zero, and is fixed forever. Reading or writing a is O(1), and arrays are the cheapest collection in memory, which is why they sit inside the other collections.
  • comes from the first .NET, before generics. It stores every item as an object, so an int is boxed: .NET allocates a new object on the heap and copies the number into it. On the way out the compiler needs a cast back, and a wrong cast is a run-time exception. Its speed also pays for the boxing and the extra objects the garbage collector (the part of .NET that frees heap objects nobody uses) must clean.
  • is the generic replacement. The compiler knows the item type, so there is no boxing and no cast, and the same code works for any type. It wraps a hidden array: is amortised O(1) because the capacity doubles when the array is full, and the indexer is O(1). and are O(n) because they scan. It is not thread-safe.
  • On a BMW FS contract feed I keep the statement lines in a because order and duplicates matter: two contracts can share a , and both must print. I only reach for an array when the count is fixed by the product, like the twelve monthly of a yearly schedule.
The interfaces, and what I return
  • The interfaces are a ladder of promises. only promises that can walk the items. adds , , , and . adds the indexer, , and . implements all of them, and offers the read parts without the changes.
  • I return the narrowest promise the caller needs. A repository that hands back its internal lets any caller call or on the cache's own data. Returning makes that call a compile error, and wraps the list so a cast back to fails; a cast to compiles, but then throws at run time. It is a wrapper, not a copy, so changes the cache makes later are still visible, which is what a cache should do.
  • On the BMW FS dealer portal, the nightly stock report asked the shared contract cache (data kept in memory for reuse) for its open contracts and then called to "free memory" after printing. It emptied the one list the portal was reading, so the portal showed zero open contracts until the next refresh. The fix was two lines: the cache returns through , and the report has no to call. A compile error is a better guard than a comment.
  • The same ladder matters for laziness. LINQ (built-in query methods like and ) is lazy: a query such as open.Where(c => c.IsSettled) does nothing until something loops it, and it runs again on the next loop. So I never loop it twice by accident; if I need the items twice, I call once, which copies them into a list, and keep that. Returning is right for a stream of results, not for a cache that must not re-query.
HashSet, equality, and the slow VIN job
  • answers one question well: is this value already here? It stores no duplicates, returns false when the value is present, and is O(1) on average. It does that by calling to pick a bucket and to confirm the match, so the type's equality is the whole contract.
  • A plain class with no or uses reference equality: two objects with the same are different keys. In a quick test, a set of plain objects kept two with the same , while a set of a (a class where two objects with the same fields count as equal) with the same field kept one. If the identity is the , key the set by the string, or give the type value equality.
  • The other trap is mutation. If an object is in a set and a field used by changes, the set looks in the new bucket and the object is lost; a test showed turn false after the changed. Keys in a set or dictionary must not change after they go in.
  • The Navistar job is the performance side. Replacing with a turns 38,000 trucks times 6,000 contracts into 38,000 hash lookups: on my test, 0.83 seconds became 6 milliseconds. The test checked the matches were identical, not just faster.
What I left behind
  • A test with duplicate and missing that runs both matchers and compares the answers, so a future "optimisation" cannot quietly change which contracts are charged.
  • A review rule: no inside a loop over another collection without a comment saying why a set or dictionary was not used; no mutation of a collection inside a ; and every performance change carries a measurement in the .

One statement run, every collection

The BMW FS month-end statement run, in the order the data meets each collection. Each type is there because of one property: fixed length, order, uniqueness, or lookup by key.

1/6The yearly schedule starts as a 12-slot array: one per month, created once.

The story

  1. Before

    BMW FS's dealer portal kept its open contracts in one shared , a store of data kept in memory so the next caller does not rebuild it: a private loaded at start-up, read by the portal on every page. A nightly stock report also needed those contracts. It asked the cache for them, printed them, and then called to free memory after the run. Within a minute the portal was showing zero open contracts to every dealer, until the next refresh loaded the cache again.

  2. Why it had to change

    is a reference type: the variable holds an address to the object, not the list. So cache.Open() handed the report the cache's own list, and open in the report was a second name for it (a ). does exactly what it says on that one list: the portal's next read found nothing. Nothing threw, because every call was legal; the code did what the developer asked, just not what they meant.

  3. How it works now

    The fix changes the promise, not the data. is an with an index and , but no or . returns a wrapper around the same list: the portal keeps reading the live cache, with no copy, while the report's stops compiling. A compile error is cheaper than an incident.

  4. What we gained

    The portal kept its contracts and the report kept working. The bigger gain is the rule: any class that owns a collection decides what callers may do with it, and returns the narrowest interface that still does the job. A caller that needs to loop gets ; one that needs to read by index gets ; only the owner keeps the .

In the lending API

The fix is the return type: the cache hands out a read-only view, not its own list.

Only the key lines are shown; Deep walks the whole file.

Lending.sln · where this file sits
  • src/Application/Contracts/
⋯ 1 line in Deep
After · ContractCache.cs · line 6 · 1 of 2 promises an index and only. There is no or to call.

The code, file by file

The cache and the report that cleared it, before and after. The contract model comes first; tap Next to follow the run.

Diagram · step 1The report asks the shared cache (data kept in memory) for its contracts: the cache's own List.
Contract.cs · line 1 · 1 of 3class: a reference type, so a variable holds a to the object, not the object itself.

What they ask next

The follow-ups interviews really ask. Answer out loud, then tap to check.

Follow-up tree

Answer, then open the next level. That is the order an interviewer digs in.

Reporteda candidate was asked Intervieweran interviewer asks it

Production bugs, and how to tell them

Each one: what you see, why, the fix in code, and the line to say.

Production incident: A third of the dealer statements never went out

P2 · serious, has a workaroundNavistar Financialcame with a deployMonth-end dealer statements batch (a .NET worker on Azure)

Composed, not a real event at Navistar Financial. The mechanism is real C#: a List<T> throws InvalidOperationException when it changes during a foreach. Dates, counts, names, the ticket, the query and the pull request are composed.

The story in 5 lines
  1. On Wed 7 Oct, Navistar Financial's month-end dealer statements run stopped at 02:14. It had written 6,240 of 9,400 contracts, so 3,160 statements were missing when the dealer portal opened.
  2. Release 5.2, deployed the evening before, had added a memory-saving step: remove a settled contract from the list as soon as its statement was written.
  3. The removal happened inside the foreach that was walking the list. List<T>'s enumerator checks a version number on every MoveNext, so the next turn threw InvalidOperationException and the died.
  4. Nothing alerted: the sent one heartbeat at the start, and nothing compared statements written with contracts due. found the same exception once and a gap of 3,160 statements.
  5. The fix moved the removal after the loop with RemoveAll, and added a test with a settled contract that asserts every statement is written.
Who's who (4 people)
You
.NET developer on the vendor team. Finds the cause, writes the fix and the test.
Tech lead
Vendor senior. Reviews the hotfix, runs the postmortem.
L2 pod
Vendor support. Checks the run, reruns the batch, confirms the statements.
Navistar dealer services lead
Owns dealer statements and dealer communications. Decides what dealers are told.

Tap a step. The map lights the place where it happens.

1/4 Detect

Detect: the job failed at 02:14. 6,240 of 9,400 statements were written; 3,160 are missing.

Wed 7 Oct, 02:14 CDT (12:44 IST)The on-call L2 pod, then you

  • What: the month-end dealer statements job stopped after 6,240 of 9,400 contracts. The 3,160 contracts that came later in the list had no statement when the portal opened at 07:00.
  • Why it hid: the sent one heartbeat at the start. No check compared statements written with contracts due, so a failed run with partial writes looked like a normal night.
  • How we saw it: showed one exception, then the stopped. compared the contracts due with the statements written.
App Insights: the exception
exceptions
| where timestamp > datetime(2026-10-07)
| where type contains "InvalidOperation"
| project timestamp, message
| take 3
In theory

A run should compare statements written with contracts due and alert on the gap, not only on the 's heartbeat.

In practice

The alert watched the 's heartbeat only, so a run that died with a partial write left its last heartbeat looking normal.

Try it yourself · 10 minutes, on your Mac
  1. dotnet new console -n ListLab. Add var nums = new List<int> { 1, 2, 3 };.
  2. In a , call nums.Remove(2) when the value is 2. Run it: InvalidOperationException: Collection was modified.
  3. Move the removal after the loop: nums.RemoveAll(n => n == 2);. Run again: no exception, and the list holds 1 and 3.
  4. Change the list to a and add an item during : in my run it throws on too. Removing the current item did not throw in my run, which is why the rule is to change nothing.
You should see

A list that changes during foreach throws at the next MoveNext. Do the change after the loop, and test with an item that triggers it.

The senior move

"Never change a list while a foreach is walking it. My fix writes every statement first, then removes settled contracts with one RemoveAll after the loop, and a test with a settled contract proves every row is written."

Sources Sourced: List<T>'s enumerator throws InvalidOperationException when the list changes during enumeration (List<T> docs; a run with a net8.0 target reproduced the message and the MoveNext frame); RemoveAll removes every match in one pass without the public enumerator (List<T> docs). Composed: Navistar's systems, dates, counts, names, the , the ticket and the query.

Traps

Traps

What should you say instead? Tap to check.

Live coding

Easy first, then harder. Set a timer and type it before you look.

Task 1

Reportedeasy · 10 minutes

Predict the three lines this program prints, and say for each whether the List or the HashSet produced it. Then say which collection kept the duplicates and which kept the input order.

Solsynch Technologies: what is HashSet?
You are given

One file. A new console app adds using System; (for ) and imports for you, which is where and live. asks the compiler to infer the type from the right-hand side.

Lending.sln · where this file sits
Stuck? Hints
  1. Does List.Add check whether the value is already there?
  2. What does HashSet.Add return when the value is already in the set?
  3. Which collection lets you read by position?
Show a solution
Lending.sln · where this file sits
Output (real run) · line 1 · 1 of 3The list holds A, B, A.
What the interviewer checks
  • 3: the list keeps both A entries.
  • 2: the set stores A once.
  • A: the first item, and the list keeps input order.
  • Can say that the set has no indexer and promises no order.

Task 2

Question index: Mustmedium · 20 minutes

Write a method that takes a List of strings and returns each that appears more than once, once each, in the order it first became a duplicate. Use a HashSet for what you have seen. Say the complexity, then say what you would change if the caller only needs a yes/no "any duplicates?".

Excellon Software: explain the concept of collections
You are given

The method and a sample call. Fill in the body; the result for this input is A,B. string.Join puts the items on one line, between commas.

Lending.sln · where this file sits
Stuck? Hints
  1. What does HashSet.Add return when the is already present?
  2. How do you avoid adding the same duplicate twice?
  3. Which collection keeps the order of first repeats?
Show a solution
Lending.sln · where this file sits
Duplicates.cs · line 1 · 1 of 9Returns the repeated in first-repeat order.
What the interviewer checks
  • Returns A,B for the sample, not A,B,A.
  • One pass: O(n) average time; the sets use O(n) extra memory.
  • Uses Add's false return as the duplicate signal, not a nested loop.
  • For yes/no, returns on the first false and keeps one set only.

Task 3

Question index: Musthard · 25 minutes

Write FirstUnique, which returns the first that appears exactly once, or null when every repeats. Keep the first-in-input order. Then say how you would handle 50 million on a machine with one gigabyte of memory.

Derived from the Wipro report: generic List vs non-generic List, which is faster
You are given

The method, a sample call and the expected answer. string? means the method may return no value (null).

Lending.sln · where this file sits
Stuck? Hints
  1. Which collection counts how many times each appears, by key?
  2. Why a second pass over the input instead of a pass over the dictionary?
  3. What does give you when the key is missing?
Show a solution
Lending.sln · where this file sits
FirstUnique.cs · line 1 · 1 of 6The ? says the return may be null: no appears once.
What the interviewer checks
  • Returns D for the sample, not the first repeated .
  • One counting pass and one input pass: O(n) time; the dictionary holds O(k) keys.
  • Uses or , never an indexer read that can throw.
  • For 50 million and one gigabyte, says a dictionary of that many strings will not fit, and proposes splitting the input by hash into small files (each group small enough to count in memory) or an external sort.
With AI tools

Assistants often write collections code from old examples, so I check four things before I keep a suggestion. First, the item type: an ArrayList or a non-generic collection in a new file is a red flag. Second, the operation: a List.Contains inside a loop over another list is O(n²), and a set or dictionary is usually the fix. Third, equality: a HashSet or Dictionary of a class only works as intended if the class has the right Equals and GetHashCode, and keys do not change after insertion. Fourth, the boundary: a method that returns List<T> lets callers clear the owner's data, so I ask for IReadOnlyList<T> or AsReadOnly. For a speed claim I ask for a Stopwatch or BenchmarkDotNet number, and for a fix I ask for a test with duplicates and a missing value.

Glossary: every name, one line each →

My notes

Paste the NOTES block from your doubt chat here. The grader never reads notes; the coach reads "Still unclear".

Read moreWhere it was asked