stride3d/stride · error · InvalidOperationException

Collection was modified after the enumerator was…

Error message

Collection was modified after the enumerator was instantiated.

What it means

SortedList's key-value enumerator tracks a version number captured at creation and compares it to the list's modificationCount on every MoveNext(). If the list was mutated (Add/Remove/Insert/Clear or indexer set) after the enumerator was created, iteration is aborted with InvalidOperationException because the underlying sort order may have shifted.

Solutions

  1. Snapshot the items first (e.g. ToArray()) and enumerate the snapshot while mutating the original
  2. Restructure the loop to collect items to remove, then remove them after the loop ends
  3. Use a for loop over indices descending (for i = Count-1; i >= 0; i--) when removing during iteration
  4. Synchronize access with a lock if another thread mutates the list

Example fix

// before
foreach (var kv in sortedList.KeyValues)
    if (kv.Value == null) sortedList.Remove(kv.Key); // throws
// after
foreach (var key in sortedList.Keys.ToArray())
    if (sortedList[key] == null) sortedList.Remove(key);
Defensive patterns

Strategy: try-catch

Try / catch

try { foreach (var kv in sortedList.KeyValues) Process(kv); } catch (InvalidOperationException ex) when (ex.Message.Contains("Collection was modified")) { /* restart with a snapshot */ foreach (var kv in sortedList.KeyValues.ToArray()) Process(kv); }

Prevention

When it happens

Trigger: Calling Add/Remove/RemoveAt/Insert/indexer-set/Clear on a SortedList between GetEnumerator() and a subsequent MoveNext() while enumerating keys and values together (the KeyValueCollection enumerator).

Common situations: Removing an item from a SortedList inside a foreach loop; a background thread mutating the collection while the UI thread iterates it; modifying the list inside a LINQ chain that is lazily evaluated.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/690c3c59ee54a2fd. Report an issue: GitHub.

Appendix: source

Thrown at sources/core/Stride.Core/Collections/SortedList.cs:832

        int idx;
        readonly int ver;

        internal KeyEnumerator(SortedList<TKey, TValue> l)
        {
            this.l = l;
            idx = NOT_STARTED;
            ver = l.modificationCount;
        }

        public void Dispose()
        {
            idx = NOT_STARTED;
        }

        public bool MoveNext()
        {
            if (ver != l.modificationCount)
                throw new InvalidOperationException("Collection was modified after the enumerator was instantiated.");

            if (idx == NOT_STARTED)
                idx = l.Count;

            return idx != FINISHED && --idx != FINISHED;
        }

        public TKey Current
        {
            get
            {
                if (idx < 0)
                    throw new InvalidOperationException();

                return l.KeyAt(l.Count - 1 - idx);
            }
        }

View on GitHub (pinned to 96fad776d2)