Back to Blog
C#

C# Dictionary Get Value: Safe Lookup Patterns

c# dictionary get value: Retrieve values from a C# Dictionary with the indexer, TryGetValue, and ContainsKey, covering null handling, comparers, and concurrency.

DictionaryTryGetValueC# CollectionsLookup PerformanceConcurrentDictionary
Illustration of a C# dictionary lookup showing key-value pairs and the indexer, TryGetValue, and ContainsKey retrieval paths.

c# dictionary get value requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you need to get a value from a C# dictionary, the API you choose determines whether a missing key throws an exception, returns a default, or silently skips the lookup. The three common approaches are the indexer, TryGetValue, and ContainsKey followed by the indexer. Each has different runtime behavior, error characteristics, and performance implications.

The Indexer and KeyNotFoundException

The indexer is the most direct way to retrieve a value:

var settings = new Dictionary<string, string> { ["timeout"] = "30" }; string timeout = settings["timeout"];

When the key exists, the indexer returns the value directly. When the key does not exist, it throws KeyNotFoundException. This is the correct behavior when a missing key represents a programming error or an invariant violation. If the key is expected to be present, the exception surfaces the problem immediately rather than allowing a default value to propagate silently through the application.

The indexer also supports ref return semantics in C# 7 and later, which matters when you need to modify a value in place for a reference type stored in the dictionary. That capability is not available through TryGetValue.

TryGetValue for a Single Lookup

TryGetValue combines existence checking and value retrieval into one operation:

if (settings.TryGetValue("timeout", out string? timeout)) { Console.WriteLine($"Timeout is {timeout}"); } else { Console.WriteLine("Timeout not configured"); }

The out parameter receives the value when the key exists, or the default value for the type when it does not. For a reference type, that default is null. For a value type, it is the zero-initialized value.

TryGetValue performs a single hash lookup. The indexer also performs a single lookup when the key exists, but ContainsKey followed by the indexer performs two lookups. In hot paths where the dictionary is large or the lookup is called frequently, the difference between one and two hash computations can be measurable, although for typical dictionary sizes it is rarely the dominant cost.

ContainsKey Before Indexing

The pattern of checking ContainsKey and then using the indexer is common in code written before TryGetValue became idiomatic:

if (settings.ContainsKey("timeout")) { string timeout = settings["timeout"]; }

This performs two hash lookups. It also has a subtle race condition in multithreaded scenarios: between the ContainsKey check and the indexer access, another thread can remove the key, causing the indexer to throw. TryGetValue avoids this window because the check and retrieval are atomic from the caller's perspective.

The main reason to use ContainsKey is when you only need to know whether the key exists and do not need the value. For example, when deciding whether to insert a new entry:

if (!settings.ContainsKey("retryCount")) { settings["retryCount"] = "3"; }

Handling Null Values Correctly

A dictionary can store null values for reference types. This creates a trap when using TryGetValue:

var cache = new Dictionary<string, string?> { ["cachedResult"] = null }; if (cache.TryGetValue("cachedResult", out string? result)) { // This branch runs, but result is null }

TryGetValue returns true when the key exists, even if the stored value is null. The out parameter is null in that case. Code that checks result != null after a successful TryGetValue will incorrectly treat a present-but-null entry as missing. If your dictionary can legitimately store null values, you need to distinguish between "key absent" and "value is null" explicitly, for example by checking ContainsKey separately or by avoiding null values in the dictionary altogether.

Custom Equality Comparers and Lookup Behavior

The lookup behavior of a dictionary is governed by its IEqualityComparer<TKey>. By default, Dictionary<string, TValue> uses ordinal case-sensitive comparison. For case-insensitive key lookup, construct the dictionary with a comparer:

var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["Content-Type"] = "application/json" }; string contentType = headers["content-type"]; // works

The comparer affects both insertion and retrieval. If you change the comparer after the dictionary is populated, existing entries become unreachable because their hash codes were computed with the old comparer. The comparer must be fixed at construction time. This is a common source of subtle bugs when a dictionary is created in one place and the comparer is assumed to be case-insensitive elsewhere.

Concurrency and ConcurrentDictionary

The standard Dictionary<TKey, TValue> is not safe for concurrent reads and writes. If one thread writes while another reads, the read can throw or return inconsistent data. ConcurrentDictionary<TKey, TValue> provides thread-safe operations including TryGetValue and the indexer.

ConcurrentDictionary also offers GetOrAdd, which atomically retrieves an existing value or adds a new one. This is useful for lazy initialization patterns where the lookup and insertion must be atomic:

var connectionCache = new ConcurrentDictionary<string, Connection>(); Connection connection = connectionCache.GetOrAdd( "primary", key => new Connection(key));

The factory delegate runs only when the key is absent. Under contention, the factory may run more than once, but only one result is stored and returned consistently. If the factory has side effects or is expensive, that repeated invocation matters, and you may need to accept it or use a different initialization strategy.

Choosing the Right Lookup API

The decision comes down to whether a missing key is an expected condition or an error:

ApproachMissing key behaviorLookupsBest use
IndexerThrows KeyNotFoundException1Key is guaranteed present
TryGetValueReturns false and default value1Missing key is a normal case
ContainsKey + indexerCheck then throw2Existence check only, or legacy code

For most new code, TryGetValue is the right default because it handles both the present and absent cases without exceptions and without a second lookup. The indexer is appropriate when a missing key is a bug that should fail fast. ContainsKey is only useful when the value is not needed at all, such as guarding an insertion. When concurrency is involved, ConcurrentDictionary with TryGetValue or GetOrAdd is the safer choice over any pattern built on the non-thread-safe Dictionary.

c# dictionary get value: Practical Usage and Code Examples | RYUSLOG DEV