Dictionary — Fast Key-Based Lookup

Dictionary<TKey, TValue> provides O(1) average-case lookup by key — far faster than searching a list by a property value. It is the go-to collection whenever you need to answer “given this key, what is the value?” questions. In ASP.NET Core you encounter dictionaries everywhere: RouteData.Values, IHeaderDictionary, IConfiguration sections, in-memory caches keyed by ID, and country-code-to-currency-symbol lookups. Understanding dictionaries thoroughly is essential for writing fast, idiomatic .NET code.

Creating and Populating Dictionaries

// ── Creating dictionaries ─────────────────────────────────────────────────
var empty    = new Dictionary<string, int>();
var capitals = new Dictionary<string, string>
{
    ["UK"]     = "London",
    ["France"] = "Paris",
    ["Germany"]= "Berlin",
};

// ── Adding entries ────────────────────────────────────────────────────────
capitals.Add("Spain", "Madrid");         // throws ArgumentException if key exists
capitals["Italy"] = "Rome";             // adds if new, updates if exists (preferred)
capitals.TryAdd("UK", "Edinburgh");     // returns false — UK already exists; does not overwrite

// ── Reading entries ───────────────────────────────────────────────────────
string london = capitals["UK"];         // direct access — throws KeyNotFoundException if missing!

// ✅ Safe access patterns
if (capitals.TryGetValue("UK", out string? capital))
    Console.WriteLine($"UK capital: {capital}");   // "London"

string? paris = capitals.GetValueOrDefault("France");      // null if not found
string  berlin = capitals.GetValueOrDefault("Germany", "Unknown");  // "Unknown" fallback

// ── Checking keys ─────────────────────────────────────────────────────────
bool hasUK   = capitals.ContainsKey("UK");      // true
bool hasRome = capitals.ContainsValue("Rome");  // true — O(n), avoid in loops!

// ── Removing entries ──────────────────────────────────────────────────────
capitals.Remove("Spain");
bool removed = capitals.Remove("Germany", out string? removedValue); // + captures value
Note: Dictionary lookup (dict[key]) uses the key’s GetHashCode() to find a bucket, then Equals() to confirm the match. This is why lookup is O(1) — no iteration required. The string type’s GetHashCode() is case-sensitive by default, so dict["UK"] and dict["uk"] are different keys. If you need case-insensitive string keys (common for HTTP header names, configuration keys, country codes), pass a comparer: new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase).
Tip: Always use TryGetValue() or GetValueOrDefault() instead of indexer access (dict[key]) when the key might not exist. The indexer throws KeyNotFoundException — a common runtime error that is easy to prevent. The pattern if (dict.TryGetValue(key, out var value)) is the idiomatic safe lookup. In ASP.NET Core, route values and query string parameters use TryGetValue extensively for exactly this reason.
Warning: Dictionary is not thread-safe. If multiple threads add, update, or remove entries concurrently, you will get corrupted internal state — silent data loss or exceptions. For shared dictionaries in ASP.NET Core (e.g., in-memory caches accessible across multiple requests), use ConcurrentDictionary<TKey, TValue> (Lesson 5) or a proper caching abstraction like IMemoryCache. Read-only dictionaries accessed after initialisation are safe for concurrent reads without locking.

Iterating Dictionaries

var scores = new Dictionary<string, int>
{
    ["Alice"] = 95, ["Bob"] = 87, ["Charlie"] = 72
};

// Iterate key-value pairs (most common)
foreach (KeyValuePair<string, int> kvp in scores)
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");

// Shorter with var deconstruction (C# 7+)
foreach ((string name, int score) in scores)
    Console.WriteLine($"{name}: {score}");

// Iterate only keys or only values
foreach (string name  in scores.Keys)   Console.WriteLine(name);
foreach (int    score in scores.Values) Console.WriteLine(score);

// LINQ on dictionaries
var topScorers = scores
    .Where(kvp => kvp.Value >= 90)
    .OrderByDescending(kvp => kvp.Value)
    .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

Common Dictionary Patterns

// ── Group items by a key — building lookup from a list ────────────────────
var posts = await _repo.GetAllAsync();
Dictionary<int, Post> byId = posts.ToDictionary(p => p.Id);

// Now O(1) lookup instead of O(n) list search:
Post? found = byId.GetValueOrDefault(42);

// ── GetOrAdd pattern — add if not present, return existing if present ─────
var cache = new Dictionary<string, List<Post>>();

// Manual GetOrAdd
if (!cache.ContainsKey("recent"))
    cache["recent"] = new List<Post>();
cache["recent"].Add(post);

// Cleaner with TryGetValue:
if (!cache.TryGetValue("recent", out var list))
    cache["recent"] = list = new List<Post>();
list.Add(post);

// ── Frequency count — count occurrences of each item ─────────────────────
string[] words = { "the", "quick", "the", "brown", "the", "fox" };
var frequency = new Dictionary<string, int>();
foreach (string word in words)
    frequency[word] = frequency.GetValueOrDefault(word) + 1;
// { "the": 3, "quick": 1, "brown": 1, "fox": 1 }

Common Mistakes

Mistake 1 — Using indexer on potentially missing key (KeyNotFoundException)

❌ Wrong:

string capital = capitals["Australia"];  // throws if not in dictionary!

✅ Correct:

if (capitals.TryGetValue("Australia", out string? cap))
    Console.WriteLine(cap);

Mistake 2 — Using case-sensitive dictionary for case-insensitive keys

❌ Wrong — “Content-Type” and “content-type” are different keys by default.

✅ Correct:

var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

🧠 Test Yourself

You have a list of 10,000 Post objects. You need to find a post by its ID repeatedly during request processing. What is the performance argument for converting the list to a Dictionary<int, Post> first?