Is ConcurrentDictionary.GetOrAdd thread-safe in C#? Using Lazy<Task<T>> to run the factory once

C# Async & Multithreading November 13, 2026 csharp

The dictionary itself won't get corrupted, but the valueFactory can run several times concurrently (only one result is kept).

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// ======================================================================
// Q: ConcurrentDictionary: is GetOrAdd really thread-safe?
//
// The dictionary itself won't get corrupted, but the valueFactory can run
// several times concurrently (only one result is kept). If the factory is
// expensive or has side effects (e.g. a token request), wrap it in Lazy, as
// shown in the code.
// - A failed Task stays cached. On error, call TryRemove, otherwise the error
//   "sticks" forever.
// - dict[key]++ on a ConcurrentDictionary is not atomic. Use AddOrUpdate(key, 1,
//   (_, v) => v + 1).
// - For a cache with expiration: HybridCache, which also has stampede protection
//   (Q42).
// ======================================================================

private readonly ConcurrentDictionary<string, Lazy<Task<ErpToken>>> _tokens = new();

public Task<ErpToken> GetTokenAsync(string tenant) =>
    _tokens.GetOrAdd(tenant, t => new Lazy<Task<ErpToken>>(() => FetchTokenAsync(t))).Value;
// Lazy (default: ExecutionAndPublication): only one FetchTokenAsync per tenant