The Rest of C# 15: The Smaller Features Worth Knowing
C# 15 lands with .NET 11 in November, and beyond union types it brings a set of small changes that remove daily friction in ordinary code. The post walks through with(...) collection expression arguments, extension indexers, labeled break and continue, and the preview redesign of unsafe — including the rules and the traps.
Key points
- with(...) must be the first and only element of a collection expression and is rejected for arrays, span types and dynamic arguments.
- Passing capacity helps only when you add items after creation; the compiler already pre-sizes when the spread source's count is known.
- Extension indexers lose to real instance indexers and to the implicit Index/Range indexers, and are never used on arrays or strings.
- An indexer over IEnumerable<T> using ElementAt can turn a loop into an O(n²) walk or execute a deferred EF Core query on every read.
- Labeled break targets loops and switch statements, labeled continue targets only loops, and the label must sit directly on that statement.
- New memory-safety rules need LangVersion preview plus the updated-memory-safety-rules switch; unsafe members now propagate to callers.
C# 15, shipping with .NET 11 in November, adds smaller features beyond union types. The most practical is with(...) as the first element of a collection expression: its arguments — named ones included — reach the collection's constructor, so [with(StringComparer.OrdinalIgnoreCase)] yields a case-insensitive HashSet<T>. Capacity is accepted for List<T> and IList<T>/ICollection<T> targets; read-only interfaces take only an empty with(), and arrays and spans are unsupported.
Extension indexers finish the extension members from C# 14: an indexer inside an extension block turns User.FindFirst(ClaimTypes.Email)?.Value into User[ClaimTypes.Email]. They are a fallback — a real instance indexer and the compiler's Index/Range indexers win, and indexers on arrays or strings are never used. Labeled break and continue exit nested loops or switches by name, while the redesigned unsafe model is preview-only behind a feature switch.
Real-world use
- An email registration service storing addresses in a HashSet<string> built with StringComparer.OrdinalIgnoreCase so logins do not fail on casing.
- An ASP.NET Core endpoint that reads claim values as User[ClaimTypes.Email] instead of chaining FindFirst()?.Value everywhere.
- A warehouse allocation routine scanning nested warehouse and bin collections that must stop at the first matching SKU.
- A custom [CollectionBuilder] type such as a TagSet that needs a comparer supplied at creation time.
Why it matters
These features cut the two-syntax split in collection creation and replace flag variables in nested loops with readable labels, while library authors doing interop get an early look at the new pointer-safety rules.