.NET 11 for Devs, Part 2: Runtime Async and Smarter C# 15 Collections

.NET 11 for Devs, Part 2: Runtime Async and C# 15 Collection Expression Arguments

This post digs into two features from the .NET 11 and C# 15 previews that change how you write and debug everyday code: runtime async, which moves async state machine handling out of the compiler and into the runtime, and collection expression arguments in C# 15, which finally give you a clean way to pass construction options into collection expressions. Both are the kind of quiet, structural change that you feel constantly once it’s on, even if it never makes a keynote slide.

Runtime Async: the compiler stops doing all the heavy lifting

For most of the history of async/await, the C# compiler has been the one responsible for turning your linear async method into a state machine. When you write something like this:

async Task<int> GetTotalAsync()
{
    var a = await FetchAsync();
    var b = await FetchAsync();
    return a + b;
}

the compiler generates a struct implementing IAsyncStateMachine, hoists every local that crosses an await boundary into fields on that struct, builds a switch over the current state, and wires up the AsyncTaskMethodBuilder to drive it. All of that IL is emitted at compile time and shipped in your assembly. It works, but it produces a lot of generated code that neither you nor the JIT ever really wanted to look at.

What runtime async actually changes

Runtime async pushes that responsibility down into the runtime (CoreCLR). Instead of the compiler fully desugaring your method into an explicit state machine, it emits IL that expresses the suspension points more directly, and the runtime is now responsible for capturing and restoring execution state when an await yields. In other words, the async “shape” becomes a first-class runtime concept rather than a compile-time transformation baked into your DLL.

Microsoft is enabling this by default in .NET 11 for a few pragmatic reasons:

  • Less generated boilerplate in your assemblies. The IL for async methods gets dramatically smaller because the state machine plumbing no longer needs to be spelled out method by method.
  • The runtime can make smarter allocation and layout decisions. Because the runtime owns suspension state, it can optimize how that state is captured — including avoiding allocations for async methods that complete synchronously, and packing state more efficiently than a compiler-generated closure struct.
  • It unblocks future optimizations. Once the runtime understands async natively, the JIT and GC can reason about async frames in ways that were impossible when everything was pre-lowered IL.

The debugging difference, concretely

The change developers will notice most immediately is debugging. Historically, stepping through async code has been a slightly out-of-body experience because you’re stepping through compiler-generated code, not the code you wrote.

Before (compiler-desugared): set a breakpoint inside an async method, hit F11 to step into an await, and you’d frequently land in MoveNext() on a generated <GetTotalAsync>d__0 struct. Call stacks were littered with framework builder frames (AsyncTaskMethodBuilder.Start, MoveNextRunner, and friends), locals showed up as hoisted fields with mangled names, and stepping across an await could bounce you through machinery that had nothing to do with your logic.

After (runtime async): because the runtime owns the state machine, the debugger and runtime can present the async method the way you wrote it. Stepping over an await behaves much more like stepping over an ordinary method call, call stacks are cleaner with fewer synthetic builder frames, and your locals appear as locals instead of hoisted state-machine fields. The mental model you have when reading the source finally matches what you see in the debugger.

On the performance side, don’t expect your app to suddenly run twice as fast — this isn’t magic. But hot async paths, especially ones that often complete synchronously (cached lookups, buffered reads), benefit from reduced allocation and tighter state handling. In allocation-sensitive services, that’s a meaningful reduction in GC pressure across the board.

Compatibility notes

Runtime async is designed to be transparent: your existing async/await code compiles and behaves the same. The observable semantics of async — ordering, exception propagation, SynchronizationContext capture — are preserved. What changes is the mechanism underneath. If you have code or tooling that reflects over compiler-generated state machine types (some profilers, source generators, or diagnostic tools do), that’s the area worth testing under the preview, since those generated types are exactly what’s going away.

C# 15: Collection Expression Arguments

Collection expressions landed in C# 12 and quickly became the default way to build collections:

int[] numbers = [1, 2, 3];
List<string> names = ["Ada", "Grace", "Marie"];
Span<int> span = [0, 1, 2, 3];

They’re clean and they work across a lot of target types. But there was always a gap: the [...] syntax gave you no way to pass construction arguments to the target type. If your collection type needed a capacity, a comparer, or any other constructor parameter, you couldn’t express it — you had to fall back to explicit construction.

The problem this solves

Consider a HashSet<string> where you want case-insensitive comparison. With collection expressions as they stood, you couldn’t say “build this set, and by the way use StringComparer.OrdinalIgnoreCase.” The comparer is a constructor argument, and collection expressions had no slot for it. So you’d write the old way:

// Old workaround — back to explicit construction
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
    "Alpha",
    "Beta",
    "Gamma"
};

Functional, but you’ve lost the collection-expression syntax entirely, and it doesn’t compose with target-typing the way [...] does.

The new syntax

C# 15 introduces a with(...) segment inside the collection expression to carry those arguments. The arguments go to the collection type’s constructor (or, for builder-based types, to the builder), while the elements populate it:

// New in C# 15
HashSet<string> set =
    [with(StringComparer.OrdinalIgnoreCase), "Alpha", "Beta", "Gamma"];

You keep the concise collection-expression form, target-typing still works, and the comparer is passed straight through to the constructor. The same mechanism applies to any collection-like type that accepts constructor arguments — capacity hints being the other common case:

// Pre-size a list you're about to fill
List<int> buffer = [with(capacity: 1024), 1, 2, 3];

// A dictionary with a custom comparer via collection expression
Dictionary<string, int> counts =
    [with(StringComparer.OrdinalIgnoreCase), ["one", 1], ["two", 2]];

Why it matters for authors of collection-like types

The bigger win is for people building their own collection types. If you use the CollectionBuilder attribute pattern to make a custom type participate in collection expressions, the argument segment gives your builder a way to receive configuration. Previously, a collection-expression-friendly type had to be constructible from elements alone; now you can accept options up front — a comparer, an allocation strategy, a pooling handle — without forcing your callers to abandon the nice syntax and drop back to explicit constructors.

Practically, that means library authors can design collection types that are both configurable and ergonomic to instantiate. That combination used to require a trade-off. It doesn’t anymore.

Reading the two features together

What ties runtime async and collection expression arguments together is a theme running through this whole release: pushing complexity to where it belongs. Runtime async moves state-machine mechanics into the runtime that’s best positioned to optimize it. Collection expression arguments move construction options into the syntax that already expresses collection creation. In both cases, you write less ceremony and the platform does more of the work.

Try it and tell us how it goes

Both features are available in the current .NET 11 and C# 15 previews. Grab the SDK, flip your test projects over, and pay particular attention to async debugging sessions and any custom collection types you own — that’s where you’ll feel the difference fastest. If you hit rough edges or have thoughts on the new syntax, file feedback on the runtime and Roslyn repos; the previews are exactly when that input shapes the final shipping behavior.

Posted on: July 30, 2026, by :