Collection Expressions & Frozen Collections in C# .NET
Master C# collection expressions and frozen collections for immutable, performant data.
20+ years shipping production .NET services in enterprise systems. Written from production experience, not tutorials.
- ✓Basic knowledge of C# and .NET
- ✓Familiarity with collections like List, Dictionary, HashSet
- ✓Understanding of immutability concepts
- Collection expressions provide concise syntax to create and initialize collections (e.g.,
List).list = [1, 2, 3]; - Frozen collections (FrozenSet, FrozenDictionary) are immutable, read-only collections optimized for fast lookup after creation.
- They improve performance in high-read, low-write scenarios and reduce memory allocations.
- Use collection expressions for cleaner code; use frozen collections for caching or configuration data.
- Both are part of modern C# and .NET (C# 12+ and .NET 8+).
Think of collection expressions as shorthand for writing a shopping list. Instead of writing 'I need to buy: milk, eggs, bread' in a long sentence, you just write the items in a list. Frozen collections are like a sealed envelope: once you put the list inside and seal it, nobody can change it, and you can quickly find any item without searching through the whole envelope.
Imagine you're building a high-traffic e-commerce API that serves product categories to thousands of clients per second. Every request reads a static list of categories. If you recreate that list on every request, you waste CPU and memory. Worse, if the list is accidentally modified, you might serve inconsistent data. This is where collection expressions and frozen collections shine. Collection expressions let you write cleaner, more expressive code when creating collections. Frozen collections take immutability to the next level: they are read-only and optimized for lightning-fast lookups. In this article, you'll learn how to use both features effectively, avoid common pitfalls, and apply them in production scenarios. We'll start with the basics, then dive into real-world examples, debugging techniques, and even a production incident story that highlights the cost of ignoring immutability.
What Are Collection Expressions?
Collection expressions are a new syntax introduced in C# 12 that allow you to create and initialize collections in a more concise and readable way. Instead of using constructors or Add methods, you can use square brackets [] to define the elements. For example, a list of integers can be created as List<int> numbers = [1, 2, 3];. This syntax works for arrays, spans, lists, and other collection types. It also supports spread operator .. to combine collections: int[] combined = [.. first, .. second];. This reduces boilerplate and makes code more declarative. Under the hood, the compiler translates these expressions into efficient code, often using collection initializers or array literals. Collection expressions are especially useful when you need to create a collection inline, such as passing arguments to a method or defining a constant set of values.
Understanding Frozen Collections
Frozen collections, introduced in .NET 8, are immutable collections that are optimized for fast read operations. They are part of the System.Collections.Frozen namespace. The two main types are FrozenSet<T> and FrozenDictionary<TKey, TValue>. Once created, you cannot add, remove, or modify elements. The 'frozen' name implies that the internal data structure is optimized for lookup, often using hash tables or other structures that are built once and never changed. This makes them ideal for scenarios where you have a fixed set of data that is read frequently, such as configuration settings, lookup tables, or static caches. Creating a frozen collection is done via extension methods . or ToFrozenSet(). on existing collections. The creation process can be expensive (O(n) time and memory), but subsequent lookups are extremely fast (O(1) average). Frozen collections are also thread-safe because they are immutable.ToFrozenDictionary()
Combining Collection Expressions with Frozen Collections
You can combine collection expressions with frozen collections to write clean, efficient code. For example, you can create a frozen set directly using a collection expression and then call ToFrozenSet(). This is especially useful when you have a small, fixed set of values. The syntax is concise: FrozenSet<int> frozen = [1, 2, 3].. Similarly for dictionaries, you can use collection expressions with key-value pairs: ToFrozenSet();FrozenDictionary<string, int> frozen = new Dictionary<string, int> { ["a"] = 1, ["b"] = 2 }.. Note that collection expressions for dictionaries require a dictionary initializer. This combination reduces boilerplate and ensures immutability from the start. It's a best practice for defining constants or configuration that should never change.ToFrozenDictionary();
Performance Considerations and Benchmarks
Frozen collections are optimized for read performance, but they come with a creation cost. In scenarios where you create the collection once and read many times, frozen collections outperform regular collections. For example, a FrozenSet lookup is typically faster than a HashSet lookup because the internal structure is optimized after freezing. However, if you only read a few times, the creation overhead may not be justified. Collection expressions themselves have negligible performance impact; they are compiled to efficient code. When benchmarking, consider the following: creation time, lookup time, and memory usage. Frozen collections often use less memory than mutable counterparts because they can discard unused capacity. Use BenchmarkDotNet to measure your specific scenario. As a rule of thumb, if you have a collection that is read more than 10 times after creation, freezing is beneficial.
Real-World Use Case: Configuration Caching
A common use case for frozen collections is caching configuration data. For example, an application might read a list of allowed IP addresses from a config file at startup. This list rarely changes, but is checked on every request. Using a FrozenSet<string> ensures fast lookups and prevents accidental modification. Similarly, a mapping of error codes to messages can be stored in a FrozenDictionary<int, string>. The following example demonstrates loading configuration from a JSON file and freezing it. Note that the frozen collection is created once and reused across requests, typically registered as a singleton in dependency injection.
IOptionsSnapshot with frozen collections recreated on change.Common Mistakes and How to Avoid Them
One common mistake is assuming that a readonly collection is immutable. readonly only prevents reassignment of the reference, not modification of the collection's contents. Another mistake is using frozen collections in scenarios where the data changes frequently; freezing is a one-time operation. Also, developers sometimes forget that ToFrozenSet() creates a copy, so modifying the original collection after freezing does not affect the frozen copy. This can lead to stale data if not handled properly. Finally, avoid using collection expressions inside loops that create many collections; this can cause excessive allocations. Instead, create the collection once outside the loop.
The Case of the Mutable Cache That Crashed Production
- Readonly does not mean immutable; it only prevents reassignment of the reference.
- Always use immutable collections for shared, read-heavy data.
- Frozen collections provide both immutability and performance.
- Test concurrent access patterns with stress tests.
- Use collection expressions to initialize frozen collections concisely.
dotnet-counters monitor --process-id <pid> System.Runtime.InteropServices.GCHandle.Countdotnet-dump analyze <dump> -c '!DumpHeap -type System.Collections.Generic.List`1'ToFrozenSet()| File | Command / Code | Purpose |
|---|---|---|
| CollectionExpressionsDemo.cs | using System; | What Are Collection Expressions? |
| FrozenCollectionsDemo.cs | using System; | Understanding Frozen Collections |
| CombinedDemo.cs | using System; | Combining Collection Expressions with Frozen Collections |
| BenchmarkExample.cs | using BenchmarkDotNet.Attributes; | Performance Considerations and Benchmarks |
| ConfigCache.cs | using System.Collections.Frozen; | Real-World Use Case |
| CommonMistakes.cs | readonly List | Common Mistakes and How to Avoid Them |
Key takeaways
Common mistakes to avoid
4 patternsUsing readonly keyword on a collection field thinking it makes the collection immutable.
Creating a frozen collection inside a loop or on every request.
Assuming that ToFrozenSet() modifies the original collection.
Using collection expressions to create large collections in hot paths without caching.
Interview Questions on This Topic
What are collection expressions in C# 12? Provide an example.
List<int> numbers = [1, 2, 3];. They also support spread operator: int[] combined = [.. a, .. b];.Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Written from production experience, not tutorials.
That's C# Advanced. Mark it forged?
3 min read · try the examples if you haven't