You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ConcurrentDictionary.GetOrCreate(key) throws ArgumentException when two threads race on a missing key #134
PR #131 (commit 01b92d0) made the ConcurrentDictionary<TKey,TVal>.GetOrCreate(key, defaultValue) overload atomic by delegating to GetOrAdd (Extensions/DictionaryExtensions.cs:65-99). The one-argument form has no ConcurrentDictionary overload, though. concurrentDict.GetOrCreate(key) binds to the IDictionary overload at Extensions/DictionaryExtensions.cs:20:
Inside that method dictionary has the static type IDictionary, so the call goes to the non-atomic IDictionary 3-argument overload (:31-58):
if(dictionary.TryGetValue(key,outTVal?val)){returnval;}dictionary.Add(key,defaultValue);// IDictionary.Add on ConcurrentDictionary throws if the key now exists
Failure scenario
Thread A calls d.GetOrCreate(1) on an empty ConcurrentDictionary<int, List<int>>, and TryGetValue misses.
Thread B does the same, and its TryGetValue also misses.
Thread A runs Add(1, listA) and succeeds.
Thread B runs Add(1, listB), which throws ArgumentException ("An item with the same key has already been added").
Expected: both callers get the stored instance, as the (key, defaultValue) overload now guarantees.
Actual: the losing caller crashes.
How verified
Built the library for net10.0 and ran a console program. In each of 2000 iterations, 8 tasks released through a Barrier all call d.GetOrCreate(1) on a fresh ConcurrentDictionary<int, List<int>>. Output:
GetOrCreate(key) on ConcurrentDictionary: 1466 ArgumentExceptions, 0 iterations with differing instances
The existing test GetOrCreateConcurrentDictionaryShouldReturnSameInstanceToParallelCallers only covers the two-argument form, so it does not catch this.
Suggested fix / acceptance criteria
Add a ConcurrentDictionary overload for the one-argument form, for example public static TVal GetOrCreate<TKey,TVal>(this ConcurrentDictionary<TKey,TVal> dictionary, TKey key) where TKey : notnull where TVal : new() => dictionary.GetOrAdd(key, _ => new TVal());, with the same null checks. Alternatively, have the IDictionary path detect ConcurrentDictionary and call GetOrAdd.
Add a test that runs Parallel.For over dictionary.GetOrCreate("key1").Add(i) on a ConcurrentDictionary<string, List<int>>. It should not throw and should end with 1000 items in one list.
Priority: High. concurrentDict.GetOrCreate(key) throws ArgumentException under contention (1466 of 2000 iterations in the repro). Make the ConcurrentDictionary GetOrCreate overload atomic #131 made the two-argument form atomic, but the one-argument form still binds to the non-atomic IDictionary path, so callers relying on that guarantee crash.
Area / suggested owner:Extensions/DictionaryExtensions.cs
Notes: Add a ConcurrentDictionary one-argument overload that uses GetOrAdd(key, _ => new TVal()), and a Parallel.For test on the one-argument form. Also audit the other IDictionary extension overloads for the same binding trap.
Activity
matt-edmondson commentedon Sep 27, 2026
Triage
concurrentDict.GetOrCreate(key)throwsArgumentExceptionunder contention (1466 of 2000 iterations in the repro). Make the ConcurrentDictionary GetOrCreate overload atomic #131 made the two-argument form atomic, but the one-argument form still binds to the non-atomicIDictionarypath, so callers relying on that guarantee crash.Extensions/DictionaryExtensions.csConcurrentDictionaryone-argument overload that usesGetOrAdd(key, _ => new TVal()), and aParallel.Fortest on the one-argument form. Also audit the otherIDictionaryextension overloads for the same binding trap.Generated by Claude Code