System.Collections.Frozen — “Create once, read forever” collections in .NET
.NET 8 introduced Frozen collections: immutable, highly-optimized lookup collections built for a very specific pattern:
Build infrequently (often once at startup) → read constantly.
The namespace is System.Collections.Frozen, and it currently ships two main types:
FrozenDictionary<TKey, TValue>FrozenSet<T>
Both are immutable and optimized for fast lookups, but they also have a higher creation cost than Dictionary / HashSet. That trade-off is the whole point.
Why Frozen exists
A regular Dictionary / HashSet is designed to be mutable: add/remove over time. That flexibility prevents some aggressive optimizations.
A Frozen collection is different: once built, it never changes, so the runtime can use more specialized internal layouts and strategies for lookup speed. Microsoft’s docs describe the intent clearly: high cost to create, excellent lookup performance, ideal for long-lived apps/services.
How to create Frozen collections
You typically create them using extension methods:
ToFrozenDictionary()ToFrozenSet()
These build the Frozen structure from an existing sequence/collection.
Example: FrozenDictionary for a hot lookup table
using System.Collections.Frozen; var httpNames = new Dictionary<int, string> { [200] = "OK", [201] = "Created", [400] = "Bad Request", [401] = "Unauthorized", [404] = "Not Found", [500] = "Server Error", }.ToFrozenDictionary(); Console.WriteLine(httpNames[404]); // "Not Found"
Example: FrozenSet for fast membership checks
using System.Collections.Frozen; var reservedKeywords = new[] { "class", "record", "struct", "interface", "public", "private", "protected", "async", "await" }.ToFrozenSet(StringComparer.Ordinal); bool isKeyword = reservedKeywords.Contains("await"); // true
Case-insensitive keys (common in configs/headers)
using System.Collections.Frozen; var headers = new[] { KeyValuePair.Create("Content-Type", "application/json"), KeyValuePair.Create("User-Agent", "my-app"), }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); Console.WriteLine(headers["content-type"]); // works
When Frozen collections shine
Use Frozen collections when all (or most) of these are true:
✅ Many reads / lookups (TryGetValue, Contains)
✅ Collection is stable (no adds/removes after creation)
✅ Built once (startup, warm-up, cache build)
✅ Used across requests / threads (great for services)
Typical real-world uses:
- Routing tables, endpoint metadata
- Token/keyword sets in parsers
- Feature flag name → handler maps
- Mapping codes → messages (status, error codes)
- Validation allow-lists / deny-lists
- Configuration keys queried repeatedly
Docs explicitly call out this “create once, use often” scenario.
When not to use Frozen
Frozen is not “faster dictionary for everything”.
Avoid it when:
❌ You mutate the collection frequently
❌ The collection is tiny and not performance-critical
❌ You rebuild it often (creation cost may dominate)
❌ You need ordered semantics or frequent enumeration-focused workloads
If you need immutability with frequent updates, ImmutableDictionary / ImmutableHashSet may fit better (persistent data structures), while Frozen is about best possible lookup after construction. (Also: Frozen is only Set/Dictionary today.)
Frozen vs alternatives (quick mental model)
Dictionary/HashSet: best general-purpose, mutableReadOnlyDictionary: read-only wrapper, not a new optimized data structureImmutableDictionary/ImmutableHashSet: immutable + efficient “modified copies”FrozenDictionary/FrozenSet: immutable + max lookup performance, but expensive build
A practical pattern: build once at startup
using System.Collections.Frozen; public static class LookupTables { public static readonly FrozenDictionary<string, int> CountryCodes = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) { ["BR"] = 55, ["US"] = 1, ["PT"] = 351 }.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase); }
Now you get:
- Fast lookups
- Thread-safe sharing (immutable)
- Zero accidental mutation
Pro tip: measure before/after
Frozen collections can be a win in hot paths, but don’t guess—benchmark with BenchmarkDotNet in your scenario (key type, size, access pattern). This is especially true because some key types already hash perfectly (e.g., ints), so gains may be smaller than with strings or complex comparers.
Wrap-up
If you have a lookup table that is:
built once + read constantly, Frozen collections are one of the cleanest “drop-in” performance upgrades in modern .NET.
#dotnet #csharp #performance #collections #backend #systemcollectionsfrozen