You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Start exercising .NET 11 previews in this repo now, while every NuGet-published library keeps
targeting net10.0. No retargeting, no consumer impact, no preview dependency forced on
contributors.
What is and isn't possible
"Use .NET 11 features" bundles three independent things. They have different answers:
Axis
Controlled by
Usable while targeting net10.0?
New C# language features
Roslyn version (SDK) + LangVersion
Yes, for syntax-only features. A feature that needs a new BCL attribute works if the attribute type is available (or declared internally); a feature that needs new CLR support (ref-struct/runtime-shaped features) hard-errors on a downlevel target.
New BCL / ASP.NET Core APIs
The TFM's targeting pack
No. Compiling against net10.0 means the net11.0 reference assemblies are not on the compile surface. The only way to use them without dropping net10.0 consumers is to multi-target net10.0;net11.0 and guard with #if NET11_0_OR_GREATER.
Running on the .NET 11 runtime
Host roll-forward at launch
Yes, today, with zero source changes. net10.0 assemblies execute on the .NET 11 runtime via roll-forward. This is where behavioral regressions show up.
So: language features and runtime validation are available immediately; new BCL APIs need
multi-targeting, and that's the one step worth deferring until there's a concrete API that earns it.
Pre-work: two things are floating today
Both should land before anyone installs a preview SDK, because both change behavior silently the
moment one is present on a machine.
There is no global.json. SDK selection is "highest installed". A contributor who installs an
.NET 11 preview for an unrelated project immediately builds this repo with it — different Roslyn,
different analyzer wave, different MSBuild — and with TreatWarningsAsErrors + EnforceCodeStyleInBuild that surfaces as build breaks that reproduce on one machine and not
another.
Directory.Build.props sets <LangVersion>latest</LangVersion>.latest is not a pinned
value — it resolves to the newest language version the current SDK's compiler fully supports. With
an .NET 11 SDK present, latest silently becomes the next C#, including its changed defaults and
new analyzer diagnostics. The language version should be an explicit, reviewed decision, not a
side effect of what someone happened to install.
Plan
Phase 0 — Pin the toolchain (no .NET 11 yet)
Add a repo-root global.json pinning the current .NET 10 SDK band, with "rollForward": "latestFeature" and — importantly — "allowPrerelease": false, so an
installed preview SDK is never picked for ordinary builds.
Replace <LangVersion>latest</LangVersion> with the explicit current version (14.0) in Directory.Build.props.
Document in CONTRIBUTING.md that a preview SDK may be installed side by side and will not
affect this repo.
Net effect: installing preview SDKs becomes a no-op for everyone else. This is the enabling step for
everything below.
Phase 1 — Run the existing net10.0 binaries on the .NET 11 runtime (non-blocking CI lane)
Cheapest, highest-signal step. No TFM change, no source change.
New workflow (.github/workflows/net11-preview.yml), schedule: nightly + workflow_dispatch, continue-on-error: true and not wired into required checks —
preview churn must never red-gate a PR.
setup-dotnet with both 10.0.x and 11.0.x (wopi-validator.yml already does exactly this
two-SDK pattern for its net8 tool, so there's precedent to copy).
Build normally, then run dotnet test with:
- DOTNET_ROLL_FORWARD=LatestMajor
- DOTNET_ROLL_FORWARD_TO_PRERELEASE=1
⚠️LatestMajor, not Major. Roll-forward policies below LatestMajor only engage when the
requested runtime is absent. Since CI installs the .NET 10 runtime, Major would quietly keep
running on .NET 10 and the whole lane would be a no-op that reports green. LatestMajor rolls
forward even when the requested version is present. _TO_PRERELEASE is required on top, because a
preview runtime is otherwise not a roll-forward candidate.
Add an assertion step that the lane actually landed on 11 (print dotnet --list-runtimes and RuntimeInformation.FrameworkDescription from a running sample) — a silently-not-rolling-forward
job is worse than no job.
Consider extending to the E2E suites later. .runsettings filters them out of default runs, so
the nightly lane would need an explicit filter override; the Collabora / ONLYOFFICE lanes are
where runtime behavior differences (HTTP stack, TLS, JSON) would actually bite.
Phase 2 — Build with the .NET 11 SDK, still targeting net10.0 (same lane)
New SDK + old TFM: new Roslyn, new analyzer wave, new MSBuild, unchanged output target.
Add a matrix leg to the Phase 1 workflow that overwrites global.json in-job (a visible,
deliberate jq step) with "allowPrerelease": true + "rollForward": "latestMajor", then
builds.
Expect findings. With TreatWarningsAsErrors globally on, every new default-on analyzer
diagnostic is a build break — that's the point of finding them nightly instead of on the
retarget branch.
Highest-risk component: infra/WopiHost.AppHost. It pins Aspire.AppHost.Sdk 13.4.6 as an
MSBuild SDK, so it's coupled to SDK internals more tightly than a normal PackageReference.
If one leg breaks first, this is it. Acceptable to exclude the AppHost from the Phase 2 leg
initially and track its Aspire-side support separately.
Phase 3 — Opt into new C# language features (still net10.0 everywhere)
Only once Phase 2 is reliably green, and ideally once the SDK is at RC/GA rather than early preview.
The important design decision is not to apply LangVersion uniformly:
Packaged libraries (src/**) stay on a releasedLangVersion. A preview language feature
can change shape or be pulled before GA; that risk does not belong in code that ships to
NuGet consumers, and it would force every contributor onto a preview SDK to build the
packages at all.
Non-shipped code (sample/**, test/**, infra/**) may move to preview first. sample/ and test/ already have their own Directory.Build.props chaining up to the root, so this is a
one-property change in each. src/ and infra/ currently have none — adding an src/Directory.Build.props (using the same GetPathOfFileAbove chaining idiom already used
twice in this repo) is what makes the split expressible.
Bumping src/** to a new releasedLangVersion requires pinning that SDK in global.json,
i.e. it makes the .NET 11 SDK mandatory for all contributors and all CI. That's a deliberate
cutover, not a drive-by.
Phase 4 — Multi-target net10.0;net11.0 (only when a specific API justifies it)
Deferred by default. Worth doing when there's a named .NET 11 API that measurably improves a hot path
— not speculatively.
Constraints to respect when it happens:
Keep the public API surface identical across both TFMs. Package validation's
compatible-framework check compares the assets in the package against each other; an API that
exists only under #if NET11_0_OR_GREATER fails it. Use the #if for implementation only.
Adding a TFM is additive and does not trip baseline validation against PackageValidationBaselineVersion (currently 9.1.0) — removingnet10.0 later is the
breaking one, and per this repo's own precedent (v8 was the last with net8.0/net9.0) that
belongs in a major.
Build/CI time roughly doubles for src/**.
CLAUDE.md states the repo is single-targeted on net10.0; that and the README need updating
in the same PR (the docs-drift workflow will flag it otherwise).
tools/wopi-validator stays on net8.0 regardless — it's pinned by the upstream Microsoft.Office.WopiValidator package.
Phase 5 — Retarget at .NET 11 GA
Standard major-version cutover: flip TFMs to net11.0, drop net10.0, bump global.json, bump all
nine workflows' dotnet-version, ship as the next major.
Notes / non-goals
Dependabot needs no guard. It doesn't propose prerelease versions while the current version is
stable, so the daily grouped-auto-merge flow won't start pulling 11.0.0-preview.* packages on its
own. That changes only if a preview package is deliberately pinned somewhere — then that project
starts receiving preview bumps.
Nothing preview goes into pull_request.yml / integrate.yml. Preview lanes are nightly,
separate, and non-blocking, for the whole life of this effort.
Not proposing any TFM change, any src/** language change, or any package-shape change as part
of Phases 0–2. Those phases are purely additive: one new file, one property made explicit, one new
non-blocking workflow.
Goal
Start exercising .NET 11 previews in this repo now, while every NuGet-published library keeps
targeting
net10.0. No retargeting, no consumer impact, no preview dependency forced oncontributors.
What is and isn't possible
"Use .NET 11 features" bundles three independent things. They have different answers:
net10.0?LangVersionnet10.0means thenet11.0reference assemblies are not on the compile surface. The only way to use them without droppingnet10.0consumers is to multi-targetnet10.0;net11.0and guard with#if NET11_0_OR_GREATER.net10.0assemblies execute on the .NET 11 runtime via roll-forward. This is where behavioral regressions show up.So: language features and runtime validation are available immediately; new BCL APIs need
multi-targeting, and that's the one step worth deferring until there's a concrete API that earns it.
Pre-work: two things are floating today
Both should land before anyone installs a preview SDK, because both change behavior silently the
moment one is present on a machine.
There is no
global.json. SDK selection is "highest installed". A contributor who installs an.NET 11 preview for an unrelated project immediately builds this repo with it — different Roslyn,
different analyzer wave, different MSBuild — and with
TreatWarningsAsErrors+EnforceCodeStyleInBuildthat surfaces as build breaks that reproduce on one machine and notanother.
Directory.Build.propssets<LangVersion>latest</LangVersion>.latestis not a pinnedvalue — it resolves to the newest language version the current SDK's compiler fully supports. With
an .NET 11 SDK present,
latestsilently becomes the next C#, including its changed defaults andnew analyzer diagnostics. The language version should be an explicit, reviewed decision, not a
side effect of what someone happened to install.
Plan
Phase 0 — Pin the toolchain (no .NET 11 yet)
global.jsonpinning the current .NET 10 SDK band, with"rollForward": "latestFeature"and — importantly —"allowPrerelease": false, so aninstalled preview SDK is never picked for ordinary builds.
<LangVersion>latest</LangVersion>with the explicit current version (14.0) inDirectory.Build.props.CONTRIBUTING.mdthat a preview SDK may be installed side by side and will notaffect this repo.
Net effect: installing preview SDKs becomes a no-op for everyone else. This is the enabling step for
everything below.
Phase 1 — Run the existing
net10.0binaries on the .NET 11 runtime (non-blocking CI lane)Cheapest, highest-signal step. No TFM change, no source change.
New workflow (
.github/workflows/net11-preview.yml),schedule:nightly +workflow_dispatch,continue-on-error: trueand not wired into required checks —preview churn must never red-gate a PR.
setup-dotnetwith both10.0.xand11.0.x(wopi-validator.ymlalready does exactly thistwo-SDK pattern for its net8 tool, so there's precedent to copy).
Build normally, then run
dotnet testwith:-
DOTNET_ROLL_FORWARD=LatestMajor-
DOTNET_ROLL_FORWARD_TO_PRERELEASE=1LatestMajor, notMajor. Roll-forward policies belowLatestMajoronly engage when therequested runtime is absent. Since CI installs the .NET 10 runtime,
Majorwould quietly keeprunning on .NET 10 and the whole lane would be a no-op that reports green.
LatestMajorrollsforward even when the requested version is present.
_TO_PRERELEASEis required on top, because apreview runtime is otherwise not a roll-forward candidate.
Add an assertion step that the lane actually landed on 11 (print
dotnet --list-runtimesandRuntimeInformation.FrameworkDescriptionfrom a running sample) — a silently-not-rolling-forwardjob is worse than no job.
Consider extending to the E2E suites later.
.runsettingsfilters them out of default runs, sothe nightly lane would need an explicit filter override; the Collabora / ONLYOFFICE lanes are
where runtime behavior differences (HTTP stack, TLS, JSON) would actually bite.
Phase 2 — Build with the .NET 11 SDK, still targeting
net10.0(same lane)New SDK + old TFM: new Roslyn, new analyzer wave, new MSBuild, unchanged output target.
global.jsonin-job (a visible,deliberate
jqstep) with"allowPrerelease": true+"rollForward": "latestMajor", thenbuilds.
TreatWarningsAsErrorsglobally on, every new default-on analyzerdiagnostic is a build break — that's the point of finding them nightly instead of on the
retarget branch.
infra/WopiHost.AppHost. It pinsAspire.AppHost.Sdk13.4.6 as anMSBuild SDK, so it's coupled to SDK internals more tightly than a normal
PackageReference.If one leg breaks first, this is it. Acceptable to exclude the AppHost from the Phase 2 leg
initially and track its Aspire-side support separately.
Phase 3 — Opt into new C# language features (still
net10.0everywhere)Only once Phase 2 is reliably green, and ideally once the SDK is at RC/GA rather than early preview.
The important design decision is not to apply
LangVersionuniformly:src/**) stay on a releasedLangVersion. A preview language featurecan change shape or be pulled before GA; that risk does not belong in code that ships to
NuGet consumers, and it would force every contributor onto a preview SDK to build the
packages at all.
sample/**,test/**,infra/**) may move topreviewfirst.sample/andtest/already have their ownDirectory.Build.propschaining up to the root, so this is aone-property change in each.
src/andinfra/currently have none — adding ansrc/Directory.Build.props(using the sameGetPathOfFileAbovechaining idiom already usedtwice in this repo) is what makes the split expressible.
src/**to a new releasedLangVersionrequires pinning that SDK inglobal.json,i.e. it makes the .NET 11 SDK mandatory for all contributors and all CI. That's a deliberate
cutover, not a drive-by.
Phase 4 — Multi-target
net10.0;net11.0(only when a specific API justifies it)Deferred by default. Worth doing when there's a named .NET 11 API that measurably improves a hot path
— not speculatively.
Constraints to respect when it happens:
compatible-framework check compares the assets in the package against each other; an API that
exists only under
#if NET11_0_OR_GREATERfails it. Use the#iffor implementation only.PackageValidationBaselineVersion(currently9.1.0) — removingnet10.0later is thebreaking one, and per this repo's own precedent (v8 was the last with
net8.0/net9.0) thatbelongs in a major.
src/**.CLAUDE.mdstates the repo is single-targeted onnet10.0; that and the README need updatingin the same PR (the
docs-driftworkflow will flag it otherwise).tools/wopi-validatorstays onnet8.0regardless — it's pinned by the upstreamMicrosoft.Office.WopiValidatorpackage.Phase 5 — Retarget at .NET 11 GA
Standard major-version cutover: flip TFMs to
net11.0, dropnet10.0, bumpglobal.json, bump allnine workflows'
dotnet-version, ship as the next major.Notes / non-goals
stable, so the daily grouped-auto-merge flow won't start pulling
11.0.0-preview.*packages on itsown. That changes only if a preview package is deliberately pinned somewhere — then that project
starts receiving preview bumps.
pull_request.yml/integrate.yml. Preview lanes are nightly,separate, and non-blocking, for the whole life of this effort.
src/**language change, or any package-shape change as partof Phases 0–2. Those phases are purely additive: one new file, one property made explicit, one new
non-blocking workflow.