.NET 10 + .NET 11 TFMs with NativeAOT and Runtime Async support#41
Conversation
Drops the net8.0 TFM (EOL 2026-11-10, same week as .NET 11 GA). Consumers on .NET 8 continue to resolve to the netstandard2.0 target. Adds <IsAotCompatible>true</IsAotCompatible> on the modern TFMs so the Roslyn AOT analyzer runs at library build time.
Since .NET SDK 8.0+, the Roslyn compiler emits source link metadata implicitly when ContinuousIntegrationBuild=true (already set on line 19). The explicit Microsoft.SourceLink.GitHub package is now a no-op. EmbedUntrackedSources and PublishRepositoryUrl remain — they still govern PDB content and pack-time URL embedding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing test suite must pass on .NET 11 with default (compiler-generated) state machines before we layer Runtime Async on top in the next commit.
Compiles the test suite with <Features>runtime-async=on</Features> on net11.0 only, so every async test method exercises our awaiters via the .NET 11 runtime-managed async lowering path. Other TFMs keep the default compiler-generated state machines.
PublishAot=true turns on the Roslyn AOT analyzer at build time; the project references the TaskTupleAwaiter library so any AOT-incompatible codegen would surface as an analyzer warning (= build error under TreatWarningsAsErrors=true). Program.cs lands in the next commit.
Program.cs awaits arity-1, arity-2 (bool and ConfigureAwaitOptions), arity-16, and the non-generic Task tuple. Each result is written to stdout to defeat linker trimming. Local build with PublishAot=true runs the Roslyn AOT analyzer over the library's full surface; no warnings emitted, confirming the source generator's output is AOT-clean. End-to-end NativeAOT publish on net10.0 + net11.0 deferred to CI (windows-latest with the standard VS Build Tools image).
setup-dotnet now installs the .NET 11 SDK alongside 8/9/10 (the older SDKs remain because the test project still targets net8.0 and net9.0). Two new steps publish the AOT smoke-test on net10.0 and net11.0; either failing fails the PR.
dotnet pack on a multi-TFM project produces one package containing all targets; only the highest SDK needs to be installed on the runner.
Updates the compatibility table to list .NET 10 and .NET 11 as the modern targets, adds Features bullets for NativeAOT compatibility and Runtime Async verification, and notes the netstandard2.0 fallback for .NET 8 / .NET 9 consumers.
Removes references to the deleted hand-authored TaskTupleExtensions.cs, documents the new AotSmokeTest project, and lists the AOT + Runtime Async TFM coverage so future sessions inherit the correct mental model.
…lish Final whole-branch review flagged two Important items: 1. SelfContained=true was in the plan's smoke-test csproj but got dropped when I dispatched Task 4. PublishAot=true implies it, but stating it explicitly closes the spec/impl paper-trail gap. 2. The CI Test step uses --no-build because Build ran with --no-restore. The two new AOT publish steps re-restored unnecessarily; --no-restore makes them consistent with the rest of the workflow.
The .NET 11 SDK elevates several IDE rules into latest-Recommended that were previously silent. Rather than rewrite working code to match the new defaults, declare the codebase's chosen style as canonical in .editorconfig. Global rules added (apply everywhere): - dotnet_style_require_accessibility_modifiers = omit_if_default:error (matches the existing 'Remove default accessibility modifiers' pass in fd8baf0) - csharp_style_namespace_declarations = file_scoped:error (file-scoped namespaces are already the codebase pattern) - csharp_prefer_braces = false:silent (allow single-statement bodies without braces, e.g. for/if in the generator) Path-scoped silences: - src/TaskTupleAwaiter.Generator/**.cs: silence CS1591 (missing XML doc) because the public surface exists for [Generator] discovery, not external consumption - test/**.cs: silence IDE0005/IDE0040/IDE0044/IDE0046/IDE0051/IDE0055 (xUnit reflection-discovered methods look unused; adapter-pattern fields aren't readonly by intent; not worth chasing down across the test suite) Also drops the explicit GenerateDocumentationFile=false override from the generator csproj — required for IDE0005 to function under .NET 11 (dotnet/roslyn#41640). Verified: full solution builds clean across all 9 TFM permutations; 2431 tests pass on net472/net8.0/net9.0/net10.0/net11.0 (net11.0 under <Features>runtime-async=on</Features>). AOT smoke-test managed compile + analyzer pass clean; native link still deferred to CI (Windows C++ toolchain missing locally).
TaskTupleAwaiter.slnx is what 'dotnet restore' (and build/test) auto-discovers when invoked at the repo root. Without listing the AotSmokeTest project in the solution, restore skipped it, so the AOT publish step in CI failed with NETSDK1004 ('project.assets.json not found') when invoked with --no-restore.
Routes the generator's 15 sb.AppendLine(...) calls through a thin wrapper sb.AppendCSharp(...) whose parameter carries [StringSyntax("C#")]. Visual Studio 2022 and JetBrains Rider read the attribute and apply C# classification + IntelliSense inside the raw-string content at every call site, making the emitted template far easier to read and edit.
Includes a single-file polyfill of System.Diagnostics.CodeAnalysis.StringSyntaxAttribute (added to the BCL in .NET 7) because the generator targets netstandard2.0. Roslyn's IDE classifiers match the attribute by namespace + type name, not by assembly identity, so an internal declaration in the project is enough.
Generator output is byte-identical (AppendCSharp delegates to AppendLine); the full test suite still passes including the net11.0 target under <Features>runtime-async=on</Features>.
There was a problem hiding this comment.
Pull request overview
This PR updates TaskTupleAwaiter to target .NET 10/.NET 11, adds NativeAOT validation via a downstream “smoke test” project, and enables .NET 11 “runtime async” compilation for the test suite to exercise the awaiters under the new lowering path.
Changes:
- Update library/test target framework matrices (add
net11.0, keep modern targets focused onnet10.0/net11.0) and mark modern TFMs as AOT compatible. - Add
TaskTupleAwaiter.AotSmokeTestconsole app and publish it in CI for bothnet10.0andnet11.0. - Improve generator authoring ergonomics (C#-annotated emit helper +
StringSyntaxAttributepolyfill) and update docs/config (README/CLAUDE, workflow SDK version, editorconfig rules).
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/TaskTupleAwaiter.Tests/TaskTupleAwaiter.Tests.csproj | Enables runtime-async=on when targeting net11.0. |
| test/TaskTupleAwaiter.AotSmokeTest/TaskTupleAwaiter.AotSmokeTest.csproj | Adds a NativeAOT-published downstream-consumer console project. |
| test/TaskTupleAwaiter.AotSmokeTest/Program.cs | Exercises key generator/awaiter codepaths (typed/non-generic, CA overloads, max arity). |
| test/Directory.Build.props | Adjusts the shared test TFM matrix. |
| TaskTupleAwaiter.slnx | Registers the new smoke test project in the solution. |
| src/TaskTupleAwaiter.Generator/TaskTupleExtensionsGenerator.cs | Switches generator emission calls to a C#-annotated append helper for IDE highlighting. |
| src/TaskTupleAwaiter.Generator/TaskTupleAwaiter.Generator.csproj | Relies on shared props for doc generation behavior. |
| src/TaskTupleAwaiter.Generator/StringSyntaxAttribute.cs | Adds a StringSyntaxAttribute polyfill for generator project targeting netstandard2.0. |
| src/TaskTupleAwaiter.Generator/CSharpEmit.cs | Adds AppendCSharp extension for emitting annotated C# strings. |
| src/Directory.Build.props | Updates library TFMs and enables <IsAotCompatible> on net10.0/net11.0; removes SourceLink package reference. |
| README.md | Updates compatibility/feature bullets and adds notes about AOT + runtime-async verification. |
| CLAUDE.md | Updates repo layout and target/framework notes to match generator-based implementation. |
| .github/workflows/release.yml | Updates release workflow SDK to 11.0.*. |
| .github/workflows/ci.yml | Updates SDK installs and adds AOT smoke-test publish steps. |
| .editorconfig | Adds/adjusts style rules and per-path diagnostic severity configuration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Agent-Logs-Url: https://github.com/buvinghausen/TaskTupleAwaiter/sessions/ab9a3590-b1e5-4f8f-849a-7a015000ac90 Co-authored-by: buvinghausen <1130210+buvinghausen@users.noreply.github.com>
Done in bf00787. I re-added Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@jnm2 I'll leave it up to you to decide but I'm assuming the juice may not be worth the squeeze for a net11.0-only code path to rewrite the awaiter internals to call AsyncHelpers.Await(Task) instead of the manual INotifyCompletion dance — that's the path for maximum runtime-native benefit (task-to-task call fusion, no unnecessary suspension). |
Agent-Logs-Url: https://github.com/buvinghausen/TaskTupleAwaiter/sessions/f76275ee-0a97-4581-a032-b628ba129cbd Co-authored-by: jnm2 <8040367+jnm2@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
I thought about this for a bit, but I don't see any way for us to make use of runtime async in this library. Runtime async comes into play when you have an async method somewhere. We just don't. Calls to AsyncHelpers.Await are generated by the compiler for each We also aren't doing anything that has runtime-async speedups. Runtime-async shines when no Task object needs to be materialized. But Task objects are already materialized in order to populate a tuple, before TaskTupleAwaiter even gets invoked. By the time TaskTupleAwaiter kicks in, there's only a Task.WhenAll to call with the already-materialized tasks, and Task.WhenAll creates a new Task object so that can't be avoided either. (It's worth noting that TaskTupleAwaiter is no worse in this regard than Personally, I would not mention runtime async at all and I would not even run tests using it. Runtime-async is only happening in .NET because it's non-breaking. So what signal could we extract from our tests that would be helpful? But, if it's important to you to have confirmed that runtime-async lives up to its goals with respect to simple uses of this library, that's fine with me. |
|
@copilot Please take the actions agreed on in each comment thread. |
Agent-Logs-Url: https://github.com/buvinghausen/TaskTupleAwaiter/sessions/5f5d1708-7b9e-4c22-88b6-46bf49e9b056 Co-authored-by: jnm2 <8040367+jnm2@users.noreply.github.com>
Completed in fc1559b: I applied the agreed changes across the open threads (library TFM/AOT condition updates plus README and CLAUDE.md corrections). Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@buvinghausen Lgtm! The NAOT stuff is great to have added. Thanks for your work keeping up with the new stuff! |
|
@jnm2 I have one last push to make claude code dumped all the no warn stuff in .editorconfig and I don't think it belongs there so I hoisted it up into the msbuild props where I think it does belong. |
|
Microsoft got real aggressive in elevating things to warnings (I think that's a good thing) in .NET 11 but this should leave us ready to go. Should I just release this as 2.2 to indicate the AOT stuff? |
|
@jnm2 I went ahead and ran with the 2.2 version you can see the full release here: https://github.com/buvinghausen/TaskTupleAwaiter/releases/tag/2.2.0 |
|
Overall the only thing that the release does is set |
|
This is because IsAotCompatible tracking wasn't added till .NET 10, and so we'd have to add a net10 build if we wanted to ship an assembly with I'm not sure what difference this makes to folks. |
|
@jnm2 I have claude running a benchmark test If that goes well and performance does get a boost I'll open a PR and let you take a look but that will put .net10 in the mix if so. |
|
That's a neat idea. M((
Task.FromResult(1),
Task.FromResult("2"),
Task.FromResult(3),
Task.FromResult("4"),
Task.FromResult(5),
Task.FromResult("6"),
Task.FromResult(7),
Task.FromResult("8")));
void M((Task<int>, Task<string>, Task<int>, Task<string>, Task<int>, Task<string>, Task<int>, Task<string>) t)
{
_ = Task.WhenAll(MemoryMarshal.CreateReadOnlySpan(
in Unsafe.As<Task<int>, Task>(ref t.Item1),
length: 8));
}
|
|
internal TupleTaskAwaiter(in (Task<T1>, Task<T2>) tasks)
{
_tasks = tasks;
_whenAllAwaiter = Task.WhenAll(
(ReadOnlySpan<Task>)MemoryMarshal.CreateReadOnlySpan(in tasks.Item1, length: 2)).GetAwaiter();
}
|
|
Ah, too bad. We can't get a span over tuple memory because ValueTuple structs are not sequential layout: So none of the above is guaranteed to work. It does work, but the runtime could break it at any time. However, we could still avoid some extra copies by using |
Summary
netstandard2.0;net462;net10.0;net11.0(dropnet8.0— EOL 2026-11-10).<IsAotCompatible>true</IsAotCompatible>so the Roslyn AOT analyzer runs at library build time.test/TaskTupleAwaiter.AotSmokeTest/— a downstream-consumer console project that exercises every generator codepath (arity-1, arity-2 with bothConfigureAwaitoverloads, arity-16, non-generic) underPublishAot=true.net11.0to the test TFM matrix and enable<Features>runtime-async=on</Features>for that target only, so every async test method exercises our awaiters via the .NET 11 runtime-managed async lowering path.net10.0andnet11.0; either failure fails the PR.11.0.*(single SDK suffices for a multi-TFMdotnet pack).TaskTupleExtensions.csmentioned there was deleted when the source generator landed in Add source generator #37).Microsoft.SourceLink.GitHubpackage — redundant under .NET SDK 8+'s implicit source link.Design and plan committed under
docs/superpowers/:docs/superpowers/specs/2026-05-12-net11-aot-design.mddocs/superpowers/plans/2026-05-12-net11-aot.mdTest Plan
net10.0locallyPublishAot=true) clean — zero warnings on the smoke-testdotnet teston the full TFM matrix (net11.0;net10.0;net9.0;net8.0;net472)dotnet publishAOT smoke-test onnet10.0(Windows native link)dotnet publishAOT smoke-test onnet11.0net11.0test target compiles under<Features>runtime-async=on</Features>and all 483 tests pass