Skip to content

Make GetOrCreate(key) atomic on ConcurrentDictionary - #136

Merged
matt-edmondson merged 2 commits into
mainfrom
fix/134-concurrent-getorcreate-key
Sep 27, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
fix/134-concurrent-getorcreate-key

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #134

What was wrong

#131 made ConcurrentDictionary.GetOrCreate(key, defaultValue) atomic. The one-argument GetOrCreate(key) had no ConcurrentDictionary overload, so it bound to the IDictionary one, which does TryGetValue and then Add. When two callers raced on a missing key, the second Add threw ArgumentException.

Change

Added GetOrCreate<TKey, TVal>(this ConcurrentDictionary<TKey, TVal>, TKey). It keeps the existing null checks and calls GetOrAdd(key, _ => new TVal()), so every caller gets the stored instance. A new TVal is only built when the key is actually missing.

Tests

  • GetOrCreateConcurrentDictionaryWithoutDefaultShouldReturnStoredValueWhenAnotherCallerAddsFirst reuses the existing RacingComparer to add a rival value between the lookup and the add, so the race happens every run. It fails without the library change and passes with it.
  • GetOrCreateConcurrentDictionaryWithoutDefaultShouldReturnSameInstanceToParallelCallers is the Parallel.For test the issue asked for. It is timing-dependent and passed in the one run without the fix, so the deterministic test above is the real regression guard.
  • dotnet test: 140/140 pass.

The triage note also suggested checking the other IDictionary extension overloads for the same binding trap. That audit is not part of this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KaXGMeYeXB1J32kSrLGhgJ


Generated by Claude Code

The one-argument GetOrCreate had no ConcurrentDictionary overload, so it bound
to the IDictionary one. That does a lookup and then Add, which throws
ArgumentException when another caller adds the key in between. Add a
ConcurrentDictionary overload that uses GetOrAdd, as the two-argument form
already does.

Fixes #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KaXGMeYeXB1J32kSrLGhgJ
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ConcurrentDictionary.GetOrCreate(key) throws ArgumentException when two threads race on a missing key

2 participants