Skip to main content

C# Frozen Collections: High-Performance Immutable Guide

Learn C# frozen collections (FrozenDictionary & FrozenSet) for blazing-fast lookups. Master .NET 8 immutable data structures with examples—start optimizing today!

C# frozen collections are one of the most underrated performance features introduced in .NET 8, and if you build read-heavy applications, they can dramatically speed up your lookups. In this tutorial, you'll learn exactly what frozen collections are, when to use FrozenDictionary and FrozenSet, how they differ from other immutable collections in C#, and the best practices that separate beginner code from production-grade, high-performance .NET applications.

Whether you're a beginner searching for "how to use frozen collections in C#", an intermediate developer looking for best practices, or a senior engineer optimizing hot paths, this guide covers everything with practical, runnable code examples.

What Are C# Frozen Collections?

C# frozen collections live in the System.Collections.Frozen namespace, which shipped with .NET 8. The two core types are FrozenDictionary<TKey, TValue> and FrozenSet<T>. A frozen collection is a special kind of immutable collection that is optimized for reading. The trade-off is simple and deliberate: creating the collection is relatively expensive, but every lookup afterwards is extremely fast.

This makes frozen collections perfect for data that you build once at application startup and then read thousands or millions of times for the lifetime of the process—configuration maps, lookup tables, route tables, feature flags, allowed-value sets, and caches that never change.

The key insight is that when a collection is created, the .NET runtime analyzes the keys and chooses an internal data layout and hashing strategy that delivers the fastest possible reads. Because the collection is guaranteed never to change, it can pre-compute these optimizations without worrying about future inserts or removals.

using System.Collections.Frozen;
using System.Collections.Generic;

var source = new Dictionary<string, int>
{
    ["alpha"] = 1,
    ["beta"] = 2,
    ["gamma"] = 3
};

// Build the frozen dictionary once.
FrozenDictionary<string, int> frozen = source.ToFrozenDictionary();

// Read it many times — this is where frozen collections shine.
if (frozen.TryGetValue("beta", out int value))
{
    System.Console.WriteLine($"beta = {value}"); // beta = 2
}

Why Use Frozen Collections in C#? (The Performance Story)

To understand why frozen collections matter, you need to understand the costs of a regular Dictionary<TKey, TValue>. A standard dictionary is designed to be balanced—fast enough for both reads and writes. But that balance means it can't fully optimize for reads alone.

Frozen collections flip that assumption. Because they are immutable, the runtime is free to:

  • Choose a specialized implementation based on the data. For example, small string-keyed dictionaries may use a layout that compares string lengths or specific characters before computing a full hash.
  • Eliminate write-path overhead. No resizing logic, no tombstones, no concurrency guards for mutation.
  • Lay out memory for cache-friendly access, reducing CPU cache misses on hot lookup paths.

The result: FrozenDictionary and FrozenSet typically deliver faster TryGetValue, Contains, and indexer lookups than Dictionary, HashSet, or ImmutableDictionary. The exact speedup depends on key type and size, but read-heavy workloads commonly see meaningful gains—and frozen string-keyed collections are especially well optimized.

The cost lives entirely in construction. Building a frozen collection is slower than building a regular dictionary because of the analysis and optimization work. That's the whole bargain: pay once at build time, save on every read.

When Frozen Collections Are the Right Choice

  • The collection is created once (often at startup) and never modified.
  • You read from it far more often than you would ever write to it.
  • Lookups sit on a hot path where latency matters (web request routing, parsing, validation).

When NOT to Use Frozen Collections

  • The data changes frequently—every change requires rebuilding the entire collection.
  • You only read the collection a handful of times; the build cost won't pay off.
  • You need add/remove semantics at runtime—use Dictionary or ConcurrentDictionary instead.

FrozenDictionary in C#: A Practical Example

FrozenDictionary<TKey, TValue> is the frozen equivalent of Dictionary<TKey, TValue>. You typically create it with the ToFrozenDictionary() extension method, which accepts an existing sequence of key-value pairs or a key selector.

using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;

public record Country(string Code, string Name);

public static class CountryLookup
{
    // Built once, shared for the lifetime of the app.
    private static readonly FrozenDictionary<string, Country> _byCode;

    static CountryLookup()
    {
        var countries = new[]
        {
            new Country("US", "United States"),
            new Country("GB", "United Kingdom"),
            new Country("CA", "Canada"),
            new Country("AU", "Australia"),
            new Country("IN", "India")
        };

        // Key selector overload: build a frozen dictionary keyed by Code.
        _byCode = countries.ToFrozenDictionary(
            keySelector: c => c.Code,
            comparer: StringComparer.OrdinalIgnoreCase);
    }

    public static Country? Find(string code) =>
        _byCode.TryGetValue(code, out var country) ? country : null;
}

// Usage
Console.WriteLine(CountryLookup.Find("ca")?.Name); // Canada

Notice two best practices in this example. First, the dictionary is stored in a static readonly field and built inside a static constructor, so the expensive freezing happens exactly once. Second, we pass an explicit StringComparer. Choosing the right comparer is important: StringComparer.Ordinal and StringComparer.OrdinalIgnoreCase are the fastest and most predictable choices for keys like codes, identifiers, and configuration keys.

FrozenSet in C#: Fast Membership Checks

FrozenSet<T> is the frozen counterpart to HashSet<T>. Use it when you need lightning-fast "does this value exist?" checks against a fixed set of values—think allow-lists, reserved keywords, valid status codes, or supported file extensions.

using System;
using System.Collections.Frozen;

public static class ReservedWords
{
    private static readonly FrozenSet<string> _keywords =
        new[] { "class", "struct", "record", "interface", "enum" }
            .ToFrozenSet(StringComparer.Ordinal);

    public static bool IsReserved(string word) => _keywords.Contains(word);
}

Console.WriteLine(ReservedWords.IsReserved("record")); // True
Console.WriteLine(ReservedWords.IsReserved("Widget")); // False

The Contains method on a FrozenSet is the operation that benefits most from freezing. If your code repeatedly validates input against a fixed set, swapping a HashSet for a FrozenSet is often a free performance win with a one-line change.

Frozen vs Immutable vs ReadOnly Collections in C#

One of the most common questions developers search for is the difference between frozen collections, immutable collections, and read-only collections in C#. They sound similar but solve different problems.

FrozenDictionary vs ImmutableDictionary

ImmutableDictionary<TKey, TValue> (from System.Collections.Immutable) is built for cheap modification: every "add" returns a new dictionary that shares most of its internal structure with the original. This is excellent for functional-style code and snapshots, but its lookups are generally slower than a regular dictionary because of the tree-based structure it uses.

FrozenDictionary is the opposite philosophy. It does not support efficient modification at all—there is no Add that returns a new instance. Instead it spends extra effort up front to make reads as fast as possible. Rule of thumb: choose immutable when you frequently derive new versions of a collection; choose frozen when the collection is set in stone and read constantly.

FrozenCollections vs ReadOnlyDictionary

ReadOnlyDictionary<TKey, TValue> and IReadOnlyDictionary are merely wrappers or interfaces that prevent callers from mutating a collection. They provide no performance benefit and the underlying data can still change through the original reference. Frozen collections, by contrast, are genuinely immutable and performance-optimized. ReadOnly is about API safety; frozen is about speed plus immutability.

Best Practices for C# Frozen Collections

  • Build once, read forever. Create frozen collections at startup and store them in static readonly fields or register them as singletons in dependency injection. Never rebuild them per request.
  • Always pick an explicit comparer for string keys. StringComparer.Ordinal or OrdinalIgnoreCase are fastest. Culture-aware comparers are slower and rarely what you want for identifiers.
  • Program against interfaces. Expose your collection as IReadOnlyDictionary<TKey, TValue> or IReadOnlySet<T> so callers don't depend on the concrete frozen type.
  • Measure before optimizing. Use BenchmarkDotNet to confirm the read gains outweigh the build cost for your specific data and access pattern.
  • Prefer TryGetValue over ContainsKey + indexer. A single lookup is faster and cleaner than two.

Common Pitfalls to Avoid

  • Rebuilding on every request. The single biggest mistake. Because construction is expensive, calling ToFrozenDictionary() inside a hot method or per HTTP request can make your code slower than a plain dictionary. Build it once.
  • Using frozen collections for changing data. If your data updates at runtime, frozen collections are the wrong tool—every change means freezing the whole thing again.
  • Forgetting the namespace and package. Frozen collections require .NET 8 or later and using System.Collections.Frozen;. They are not available in .NET Framework or older .NET versions.
  • Expecting magic for tiny, rarely-read collections. If you only read a few times, the build cost dominates and there's no benefit.

A Realistic ASP.NET Core Use Case

Frozen collections pair perfectly with dependency injection. Register the frozen collection as a singleton so it's built exactly once when the app starts, then inject it wherever you need fast lookups.

using System.Collections.Frozen;

// Program.cs (ASP.NET Core)
var builder = WebApplication.CreateBuilder(args);

FrozenDictionary<string, decimal> taxRates = new Dictionary<string, decimal>
{
    ["US"] = 0.07m,
    ["GB"] = 0.20m,
    ["CA"] = 0.05m,
    ["AU"] = 0.10m,
    ["IN"] = 0.18m
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);

// Singleton: built once, read on every request.
builder.Services.AddSingleton(taxRates);

var app = builder.Build();

app.MapGet("/tax/{country}", (string country, FrozenDictionary<string, decimal> rates) =>
    rates.TryGetValue(country, out var rate)
        ? Results.Ok(new { country, rate })
        : Results.NotFound());

app.Run();

Here, the tax-rate lookup is read on every single API call but never changes while the app runs—an ideal scenario for a FrozenDictionary.

Conclusion: Key Takeaways on C# Frozen Collections

C# frozen collections are a focused, high-impact tool for read-heavy .NET applications. By accepting a higher one-time construction cost, FrozenDictionary and FrozenSet give you some of the fastest lookups available in the framework for immutable data.

Here are the key takeaways to remember:

  • Use frozen collections for "build once, read many" data—lookup tables, config maps, allow-lists, and routing data.
  • FrozenDictionary replaces Dictionary and FrozenSet replaces HashSet when the data never changes.
  • Choose frozen for fast reads, immutable for cheap derived versions, and read-only for API safety.
  • Always build them once (static field or DI singleton) and use an ordinal StringComparer for string keys.
  • Frozen collections require .NET 8 or later and the System.Collections.Frozen namespace.

Next time you spot a dictionary or set that's populated at startup and read on every request, try swapping in a frozen collection and benchmark the difference. For many real-world C# applications, it's one of the simplest performance wins you can make today.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...