Quick: how do you create a List<int> with three items? new List<int> { 1, 2, 3 }? Or maybe new List<int>([1, 2, 3])? An array? new int[] { 1, 2, 3 }. A span? stackalloc int[] { 1, 2, 3 }. An immutable array? ImmutableArray.Create(1, 2, 3).

Every collection type had its own ceremony. C# 12 introduces collection expressions — one syntax that targets them all:

// All of these use the same [1, 2, 3] syntax
int[] array = [1, 2, 3];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3];
ReadOnlySpan<int> roSpan = [1, 2, 3];
ImmutableArray<int> immutable = [1, 2, 3];
HashSet<int> set = [1, 2, 3];

The compiler figures out what to emit based on the target type. For Span<int>, it stack-allocates. For List<int>, it creates a list with the right capacity. For ImmutableArray<int>, it uses the builder. You don’t need to know the optimal construction API for each type. The compiler does.

The Spread Operator

Need to combine collections? The .. spread operator inlines one collection into another:

int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] combined = [..first, ..second]; // [1, 2, 3, 4, 5, 6]
int[] withExtra = [0, ..first, ..second, 7, 8]; // [0, 1, 2, 3, 4, 5, 6, 7, 8]

This replaces the Concat + ToArray pattern, and the compiler can optimize it into a single allocation with known size.

Practical Example: Building Middleware Pipelines

public static IEnumerable<string> GetCorsOrigins(bool isDevelopment)
{
string[] production = ["https://app.example.com", "https://admin.example.com"];
string[] development = ["http://localhost:3000", "http://localhost:5173"];
return isDevelopment
? [..production, ..development]
: production;
}

No Enumerable.Concat(). No .ToArray(). No LINQ allocation chain. Just a clean expression.

Empty Collections

The empty collection literal [] is also the new best way to return or initialize an empty collection:

// Before: Array.Empty<string>(), new List<string>(), Enumerable.Empty<string>()
// After:
string[] empty = [];
List<string> emptyList = [];
ImmutableArray<string> emptyImmutable = [];

The compiler emits the most efficient empty representation for each target type (Array.Empty<T>() for arrays, a cached empty instance for immutable arrays, etc.).

Key Takeaway

[1, 2, 3] works for arrays, lists, spans, immutable collections, and sets. The .. spread operator combines them without LINQ overhead. One syntax, all collection types, zero ceremony. If you’re still writing new List<int> { ... }, you’re writing more characters than you need to.