C# / Editorial solution guide

Resolve Collection, Concurrency, and Lifetime Errors

Distinguish invalid enumeration, shared-state races, and expired resource leases before choosing a contextual repair.

About 3 min readContent reviewed

Read-only knowledge base · No public comments

Recognize the symptoms

A list operation reports InvalidOperationException during enumeration, a shared counter loses updates, an asynchronous consumer sees reused buffer data, or a stream throws ObjectDisposedException. These symptoms can originate from different contracts. A thread-safe collection does not repair a disposed dependency, and ToArray does not make a concurrent unsafe enumeration valid automatically.

Understand the likely causes

  • A mutable collection can be structurally changed while an enumerator is active. Aliases can share a list, map, or nested element even after an outer copy. Separate reads and writes can violate a compound invariant, and pooled/disposable storage can outlive its lease when work is launched without joining. Identify which of these applies before replacing containers indiscriminately.

1. Trace storage and ownership

List the actual collection instance, its aliases, active enumerators, writers, and resource owner. Inspect where a stream is disposed or a pooled array is returned. A local variable name does not imply independent storage. Reduce the scenario to synthetic local data and preserve the first useful stack rather than suppressing every exception around the loop.

2. Separate enumeration from structural mutation

Many collection enumerators detect structural invalidation; exact allowances are collection-specific. In a single-owner list, collect the intended removals before mutating, or use an index strategy whose bounds are proved. The example materializes selected integer values before removal. This is safe here because no other thread accesses the list; it is not a general concurrent snapshot protocol.

csharp Two phases under single-owner access
using System;
using System.Collections.Generic;
using System.Linq;

var values = new List<int> { 1, 2, 3, 4 };
int[] removals = values.Where(value => value % 2 == 0).ToArray();
foreach (int value in removals) values.Remove(value);
Console.WriteLine(string.Join(",", values));
Expected output
1,3

The removal work list is independent of the list enumerator used later for mutation.

3. Synchronize the whole shared operation

For concurrent mutation, protect every reader and writer under one lock or use a documented concurrent design. A final await does not repair updates that raced while workers ran. ConcurrentDictionary makes specific operations safe, but ContainsKey followed by an update is not automatically one transaction. GetOrAdd/AddOrUpdate factories may repeat, so avoid exactly-once side effects inside them.

4. Extend the lease through actual completion

An async task or deferred query can keep using a resource after its creator returns. Await within the owning scope or transfer ownership. Return an ArrayPool buffer only after all borrowers finish, and dispose a stream or semaphore only after users stop. ReadOnlyMemory limits writes through one view but does not stop another owner from reusing the backing array.

5. Match snapshot depth to the promise

ToArray and new List<T>(source) copy element values; mutable reference elements can remain shared. If callers need a stable independent report, project immutable values or copy the required fields under valid synchronization. IReadOnlyList limits a caller’s operations through that interface, not every alias. Document ordering separately and sort explicitly when deterministic presentation matters.

Verify the fix

The complete example must print1,3. Add tests for empty input, no removals, all removals, duplicate values, and documented ordering. For shared variants, coordinate worker starts and await every completion, then verify the complete invariant. For lifetime variants, delay consumption with an explicit completion signal and prove disposal or pool return happens afterward. Do not treat repeated noncrashing runs as a universal safety proof.

Keep it from recurring

  • Keep mutable storage private, document copy depth and borrowing duration, and separate iteration from updates where appropriate. Use one coherent synchronization policy and observe task completion before releasing dependencies. Review every early return for active enumerators, queued work, and outstanding resource borrowers.

Official references