.NET 11 for Devs, Part 2: Two More Features Worth Your Attention
Welcome back to our .NET 11 for devs series. In this instalment we’re digging into two features that sit at different levels of the stack but both matter if you write a lot of everyday C#: runtime async, which changes how async methods are compiled and executed, and collection expression arguments in C# 15, a small syntax addition with some genuinely useful ergonomics.
Runtime Async
Since C# 5, async/await has been a compiler trick. The compiler rewrites your async method into a state machine: a generated struct that tracks where execution paused, captures locals, and hands control back to the caller when it hits an await that isn’t already complete. It works well, but it’s a lot of generated code, and it produces allocations and indirection the runtime never really understood as “async” — it just saw ordinary types and method calls.
Runtime async moves that responsibility down into the runtime itself. Instead of the compiler emitting a full state machine, it emits async methods that the JIT and runtime recognise as suspendable. The runtime handles the suspension and resumption directly, which means it can make smarter decisions about how state is captured and how continuations are scheduled.
Why you’d care day-to-day
- Fewer allocations on hot async paths. The runtime can avoid boxing and heap-allocating machinery in cases where the compiler previously couldn’t. For services that push a lot of async calls per second, that’s less GC pressure.
- Smaller code size. Less generated state-machine IL means smaller assemblies and, in AOT scenarios, smaller output.
- Better inlining and optimisation. Because the runtime understands the async boundary, the JIT has more room to optimise around it rather than treating a generated struct as an opaque blob.
The important practical point: your existing async code doesn’t need to change. You still write async and await the same way, still return Task and ValueTask, and the semantics are the same. This is an implementation change beneath the language, not a new programming model. You should think of it as “the same async you already write, executed more efficiently.”
A couple of caveats worth keeping in mind. First, the performance win is most visible in async-heavy workloads — a method that awaits once and returns won’t show a dramatic difference. Second, if you have code that reaches into the generated state machine or relies on its exact shape (some diagnostics and profiling tooling does), verify it still behaves as expected after upgrading. For the vast majority of application code, though, this is a free improvement you get simply by targeting .NET 11.
Collection expression arguments in C# 15
Collection expressions arrived in C# 12 and quickly became a default habit: int[] xs = [1, 2, 3]; reads better than the old initialiser syntax and works across arrays, spans, and most collection types. What they didn’t let you do was influence how the target collection was constructed. If a collection type needed a comparer, a capacity, or some other constructor argument, you were back to the explicit constructor call.
C# 15 closes that gap with collection expression arguments. You can now pass constructor-style arguments to a collection expression using a with(...) clause, and those arguments flow through to the underlying collection’s construction.
A quick example
Say you want a HashSet<string> that ignores case. Previously you’d write:
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Alice", "bob", "CAROL"
};
With collection expression arguments you can keep the concise collection-expression form while still supplying the comparer:
HashSet<string> names = [with(StringComparer.OrdinalIgnoreCase), "Alice", "bob", "CAROL"];
The with(...) clause comes first and carries the arguments; the rest are the elements. The set is built with the comparer, so "ALICE" and "alice" are treated as duplicates as you’d expect.
Why it’s useful
- Consistency. You no longer have to drop out of collection-expression syntax the moment a collection needs a constructor argument. One style covers more cases.
- Capacity hints. For dictionaries and lists where you know the size up front, you can pass an initial capacity and avoid reallocations without abandoning the terse syntax.
- Custom collection types. If you build your own collections that take construction parameters, they can participate in this syntax through the same collection-builder mechanism collection expressions already use.
It’s a modest feature, and it won’t change how you architect anything. But it removes one of the small friction points that made collection expressions feel incomplete, and it’s the kind of thing you’ll reach for without thinking once it’s in your toolbox.
Should you adopt them?
These two features sit at opposite ends of the effort spectrum. Runtime async needs nothing from you beyond targeting .NET 11 — the benefit shows up on async-heavy paths automatically, and it’s low-risk for typical application code. Collection expression arguments require a language version bump to C# 15 and a small change in habit, but they’re purely additive and safe to introduce gradually.
If you’re already planning a move to .NET 11, both are easy wins. We’ll keep working through the release in the next instalment.