diff --git a/.editorconfig b/.editorconfig
index a9b39cb..229c475 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,4 +1,4 @@
-# Purview.BuildSdk 2.0.0 - managed file, do not edit
+# Purview.BuildSdk 2.0.1 - managed file, do not edit
#
# This file is written by Purview.BuildSdk and is replaced whenever the SDK version changes. The version
# on the first line is how the SDK recognises a file it owns: a .editorconfig without that marker is
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a49fbf..96e5fd7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,18 @@ release summarise what the commits recorded rather than what was written at the
## [Unreleased]
+## [2.0.1] - 2026-11-12
+
+### Changed
+
+- **Runtime Async is off by default.** A `net11.0`+ target no longer compiles with
+ `runtime-async=on` unless asked: it changes how every `await` compiles, so it
+ is an opt-in rather than a baseline. Set `PurviewRuntimeAsync=true` — or `UseRuntimeAsync=true`,
+ the name the .NET team uses — to enable it; an explicit `false` under either name keeps it off.
+ This reverts the 2.0.0 default, so a repository that relied on it should opt in.
+
+## [2.0.0] - 2026-11-11
+
### Added
- **`ServerShared` projects.** A `ServerShared*` project is referenced automatically by every
diff --git a/README.md b/README.md
index 3adc17d..2717e48 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def
| Feature | Detail |
| -- | -- |
| **Project type detection** | `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsServerSharedProject`, `IsClientSideProject`, `IsContainerProject`, `IsWebSdkProject`, `IsAspireHostProject`, `IsBlazorWebAssemblyProject`, `IsBffProject`, … |
-| **.NET 11 opt-ins** | A `net11.0` target compiles with Runtime Async (`runtime-async=on`) and the C# 15 updated memory-safety rules (`MemorySafetyRules=2`), while `AllowUnsafeBlocks` stays at its compiler default (`false`); builds use MSBuild's multithreaded mode through a bootstrapped `Directory.Build.rsp` (see [.NET 11 features](#net-11-features)) |
+| **.NET 11 opt-ins** | A `net11.0` target compiles with the C# 15 updated memory-safety rules (`MemorySafetyRules=2`) and, when opted in with `PurviewRuntimeAsync=true`, Runtime Async (`runtime-async=on`), while `AllowUnsafeBlocks` stays at its compiler default (`false`); builds use MSBuild's multithreaded mode through a bootstrapped `Directory.Build.rsp` (see [.NET 11 features](#net-11-features)) |
| **ServerShared** | A `ServerShared*` project is referenced automatically by every non-client-side project, and never by a WebAssembly client, browser-safe library, Razor class library or `*.Client`-style project; `PRSHR0001`/`PRSHR0002` report a reference that would still put it in a browser download (see [ServerShared](#servershared)) |
| **Blazor and BFF** | Blazor WebAssembly clients, Blazor Web App hosts and BFF hosts are detected; `PurviewBrowserSafe` makes a library the client downloads fail the build on trim, AOT and browser-unsupported APIs, and the client checks every reference sets it (see [Blazor WebAssembly and BFF](#blazor-webassembly-and-bff)) |
| **C# defaults** | `net10.0;net11.0` TFMs (overridable), `LangVersion=latest` (C# 15), `Nullable=enable`, `ImplicitUsings=enable`, deterministic builds |
@@ -44,7 +44,7 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def
"runner": "Microsoft.Testing.Platform"
},
"msbuild-sdks": {
- "Purview.BuildSdk": "2.0.0"
+ "Purview.BuildSdk": "2.0.1"
}
}
```
@@ -203,7 +203,7 @@ to a low-importance message) and only a copy that still fails after every retry
The bootstrapped `.editorconfig` opens with a version marker:
```editorconfig
-# Purview.BuildSdk 2.0.0 - managed file, do not edit
+# Purview.BuildSdk 2.0.1 - managed file, do not edit
```
That line is how the SDK recognises a file **it** wrote. When the SDK version changes, a file carrying
@@ -378,7 +378,7 @@ Version detection logging is disabled by default. Set `VersionDetectionLogEnable
| `AssemblyIdentifierClassName` | `Assembly` | Overrides the name of the generated assembly identifier marker class. |
| `AutoIncludeUsings` | `true` | Controls SDK-added global usings for `NamespacePrefix` and `RootNamespace`. |
| `MemorySafetyRules` | `2` | C# 15 memory-safety tier for `net11.0`+ targets: `0` off, `1` preview pointer syntax, `2` updated rules (`updated-memory-safety-rules`, the default). `AllowUnsafeBlocks` is never changed. |
-| `PurviewRuntimeAsync` | `true` | Compiles `net11.0`+ targets with `runtime-async=on`. `UseRuntimeAsync=false` also opts out. |
+| `PurviewRuntimeAsync` | `false` | Set to `true` to compile `net11.0`+ targets with `runtime-async=on`. `UseRuntimeAsync=true` also opts in; an explicit `false` under either name keeps it off. |
| `IsClientSideProject` | *(detected)* | `true` for a project whose assemblies are downloaded to a browser — a WebAssembly client, `PurviewBrowserSafe` library, Razor class library, or `*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` name. Set explicitly to override; a `ServerShared` project is never added to a client-side project. |
| `PurviewAutoServerSharedReference` | `true` | Adds `../ServerShared*/ServerShared*.csproj` to a non-client-side project. `false` removes the automatic reference. |
| `DisablePurviewServerSharedReferenceCheck` | `false` | Suppresses `PRSHR0001`/`PRSHR0002`. |
@@ -583,7 +583,7 @@ later) target, so the `net10.0` leg of the default `net10.0;net11.0` set is unaf
| Feature | Applied | Why |
| -- | -- | -- |
-| Runtime Async V2 | `runtime-async=on` | .NET 11 no longer requires `EnablePreviewFeatures`, and the runtime libraries ship compiled with it. Cleaner live stack traces, lower await overhead, and continuations that skip `ExecutionContext` when nothing is ambient. Opt out with `PurviewRuntimeAsync=false` or `UseRuntimeAsync=false`. |
+| Runtime Async V2 (opt-in) | `runtime-async=on` when enabled | Enables Runtime Async for a target where the team has decided it is wanted: cleaner live stack traces, lower await overhead, and continuations that skip `ExecutionContext` when nothing is ambient. It is **off by default** because it changes how every `await` compiles; opt in with `PurviewRuntimeAsync=true` or `UseRuntimeAsync=true`. An explicit `false` under either name keeps it off. |
| C# 15 updated memory-safety rules | `updated-memory-safety-rules` (`MemorySafetyRules=2`) | `unsafe` marks requires-unsafe APIs and the compiler records the choice in `MemorySafetyRulesAttribute`. Independent of `LangVersion`, so the default `latest` is enough. |
| Unsafe code | **not** enabled | `AllowUnsafeBlocks` stays at the compiler default (`false`). The updated model is strictest with unsafe code disallowed; a project that needs pointers opts in itself. |
| Multithreaded MSBuild | `Directory.Build.rsp` with `-mt` | Runs a build's projects concurrently in one process, which is 20–26% faster on large solutions per the MSBuild measurements. `-mt` is a host switch, so it cannot be set from a project; the response file is the only mechanism. `-nr:false` accompanies it because a node left by a non-`-mt` build can cache an `RoslynCodeTaskFactory` task with no assembly path and fail the next `-mt` build (`dotnet/msbuild#15213`); the short-lived server still gives Server GC. |
diff --git a/docs/wiki/Configuration-Reference.md b/docs/wiki/Configuration-Reference.md
index 0e56da2..2bac8d1 100644
--- a/docs/wiki/Configuration-Reference.md
+++ b/docs/wiki/Configuration-Reference.md
@@ -37,7 +37,7 @@ See [Version Detection](Version-Detection.md) for the full resolution rules.
| `AssemblyIdentifierClassName` | `Assembly` | Overrides the name of the generated assembly identifier marker class. |
| `AutoIncludeUsings` | `true` | Controls SDK-added global usings for `NamespacePrefix` and `RootNamespace`. |
| `MemorySafetyRules` | `2` | C# 15 memory-safety tier on `net11.0`+ targets: `0` off, `1` preview pointer syntax (sets `LangVersion=preview` when the project has not chosen one), `2` updated rules (`updated-memory-safety-rules`). An unrecognised value fails with `PRCFG0001`. `AllowUnsafeBlocks` is never changed. |
-| `PurviewRuntimeAsync` | `true` | Adds `runtime-async=on` on `net11.0`+ targets. `PurviewRuntimeAsync=false` or `UseRuntimeAsync=false` opts out. |
+| `PurviewRuntimeAsync` | `false` | Set to `true` to add `runtime-async=on` on `net11.0`+ targets. `UseRuntimeAsync=true` also opts in; an explicit `false` under either name keeps it off. |
| `IsClientSideProject` | *(detected)* | `true` when the project is downloaded to a browser: a WebAssembly client, a `PurviewBrowserSafe` library, a Razor class library, or a `*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` name. Set explicitly to override the detection either way. |
| `PurviewAutoServerSharedReference` | `true` | Adds `../ServerShared*/ServerShared*.csproj` to every non-client-side project. `false` disables it. |
| `DisablePurviewServerSharedReferenceCheck` | `false` | Turns off `PRSHR0001` and `PRSHR0002`. |
@@ -186,7 +186,7 @@ unchanged.
| Property | Applied | Notes |
| -- | -- | -- |
-| `PurviewRuntimeAsync` | `true` | `runtime-async=on`. `UseRuntimeAsync=false` also opts out. |
+| `PurviewRuntimeAsync` | `false` | When set to `true`, adds `runtime-async=on`. `UseRuntimeAsync=true` also opts in; an explicit `false` under either name keeps it off. |
| `MemorySafetyRules` | `2` | `updated-memory-safety-rules`; `1` sets `LangVersion=preview` for the pointer-syntax tier; `0` disables. `PRCFG0001` on an unknown value. |
| `AllowUnsafeBlocks` | *(compiler default: false)* | Never set by the SDK. |
@@ -301,7 +301,7 @@ read them through `build_property.`:
| `DisablePurviewServerSharedReferenceCheck` | When `true`, suppresses `PRSHR0001`/`PRSHR0002`. |
| `PurviewServerSharedReferenceCheckAsError` | When `true`, reports `PRSHR0001`/`PRSHR0002` as errors. |
| `MemorySafetyRules` | C# 15 memory-safety tier applied to `net11.0`+ targets. |
-| `PurviewRuntimeAsync` | When `true` (default), `net11.0`+ targets compile with `runtime-async=on`. |
+| `PurviewRuntimeAsync` | When `true`, `net11.0`+ targets compile with `runtime-async=on`. Defaults to `false`; `UseRuntimeAsync=true` also opts in. |
| `PurviewMultiThreadedBuild` | When `true` (default), a managed `Directory.Build.rsp` enables MSBuild `-mt`. |
| `EditorConfigFilePath` | Path to the SDK-provided `.editorconfig` injected into `@(EditorConfigFiles)`. |
| `RepositoryEditorConfigFilePath` | Destination path for bootstrapping a physical repo-level `.editorconfig`. |
diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md
index 1b3c797..734d6b1 100644
--- a/docs/wiki/Getting-Started.md
+++ b/docs/wiki/Getting-Started.md
@@ -14,7 +14,7 @@ Add `Purview.BuildSdk` to the `msbuild-sdks` section so MSBuild can resolve the
"runner": "Microsoft.Testing.Platform"
},
"msbuild-sdks": {
- "Purview.BuildSdk": "2.0.0"
+ "Purview.BuildSdk": "2.0.1"
}
}
```
diff --git a/package.json b/package.json
index e361a6d..0d62d59 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "build-sdk",
- "version": "2.0.0",
+ "version": "2.0.1",
"homepage": "https://purview.dev/projects/build-sdk/",
"bugs": {
"url": "https://github.com/purview-dev/build-sdk/issues"
diff --git a/skills/purview-buildsdk-configuration/SKILL.md b/skills/purview-buildsdk-configuration/SKILL.md
index b540704..fca74cc 100644
--- a/skills/purview-buildsdk-configuration/SKILL.md
+++ b/skills/purview-buildsdk-configuration/SKILL.md
@@ -51,7 +51,7 @@ in `Directory.Build.props` unless the setting is deliberately project-specific.
| `IsBffProject` | name ends `Bff` | Classifies a BFF host; `false` opts out |
| `PurviewBrowserSafeReferenceCheckAsError` | `false` | Makes `PRBLZ0001` (a client reference that is not browser-safe) an error |
| `MemorySafetyRules` | `2` | C# 15 memory-safety tier on `net11.0`+: `0` off, `1` preview syntax, `2` updated rules |
-| `PurviewRuntimeAsync` | `true` | `runtime-async=on` on `net11.0`+ |
+| `PurviewRuntimeAsync` | `false` | Opts a `net11.0`+ target into `runtime-async=on`; `UseRuntimeAsync=true` also opts in |
| `IsClientSideProject` | *(computed)* | Whether the project's assemblies can reach a browser; overrides the `ServerShared` classification |
| `PurviewAutoServerSharedReference` | `true` | Adds a sibling `ServerShared` reference to a non-client-side project |
| `PurviewMultiThreadedBuild` | `true` | Bootstraps a `Directory.Build.rsp` enabling MSBuild `-mt` |
diff --git a/skills/purview-buildsdk-configuration/references/properties.md b/skills/purview-buildsdk-configuration/references/properties.md
index 82f84d7..8a8f139 100644
--- a/skills/purview-buildsdk-configuration/references/properties.md
+++ b/skills/purview-buildsdk-configuration/references/properties.md
@@ -41,7 +41,7 @@ consuming project supplies nothing.
| Property | Default | Behaviour |
| -- | -- | -- |
| `MemorySafetyRules` | `2` | C# 15 memory-safety tier on `net11.0`+ targets: `0` off, `1` preview pointer syntax (sets `LangVersion=preview` when none was chosen), `2` `updated-memory-safety-rules`. Unknown values fail with `PRCFG0001`. Never touches `AllowUnsafeBlocks` |
-| `PurviewRuntimeAsync` | `true` | `runtime-async=on` on `net11.0`+ targets. `UseRuntimeAsync=false` also opts out |
+| `PurviewRuntimeAsync` | `false` | Set to `true` for `runtime-async=on` on `net11.0`+ targets. `UseRuntimeAsync=true` also opts in; an explicit `false` under either name keeps it off |
| `PurviewMultiThreadedBuild` | `true` | Bootstraps a managed `Directory.Build.rsp` with `-mt`/`-nr:false`; `false` writes `-mt:false` |
| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Whether that bootstrapped response file is written at all |
| `IsClientSideProject` | *(computed)* | `true` for a WebAssembly client, `PurviewBrowserSafe` library, Razor class library, or `*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` name. Explicit value wins |
diff --git a/src/src/BuildSdk/Sdk/Props/Defaults.props b/src/src/BuildSdk/Sdk/Props/Defaults.props
index 6733b4e..931fe8b 100644
--- a/src/src/BuildSdk/Sdk/Props/Defaults.props
+++ b/src/src/BuildSdk/Sdk/Props/Defaults.props
@@ -196,7 +196,9 @@
and PRSHR0002). Warning by default; set this to true to fail the build instead. -->
false
- true
+
true
diff --git a/src/src/BuildSdk/Sdk/Sdk.props b/src/src/BuildSdk/Sdk/Sdk.props
index e8a2b73..9f1e380 100644
--- a/src/src/BuildSdk/Sdk/Sdk.props
+++ b/src/src/BuildSdk/Sdk/Sdk.props
@@ -564,7 +564,7 @@
The C# 15 memory-safety tier: 0 off, 1 preview syntax, 2 updated rules (default). AllowUnsafeBlocks is left unset.
- When true (default), a net11.0+ project compiles with the runtime-async compiler feature. UseRuntimeAsync=false opts out.
+ When true, a net11.0+ project compiles with the runtime-async compiler feature. Defaults to false; UseRuntimeAsync=true also opts in.
When true (default), boots a Directory.Build.rsp that enables MSBuild's multithreaded build (-mt).
diff --git a/src/src/BuildSdk/Sdk/Targets/Modern.targets b/src/src/BuildSdk/Sdk/Targets/Modern.targets
index 3a155db..c84ef10 100644
--- a/src/src/BuildSdk/Sdk/Targets/Modern.targets
+++ b/src/src/BuildSdk/Sdk/Targets/Modern.targets
@@ -7,15 +7,15 @@
property. That matters: TargetFrameworks defaults to net10.0;net11.0, so
a switch that improves net11.0 must not be applied to the net10.0 leg.
- Two compiler features are enabled for a net11.0+ target:
+ One compiler feature is enabled by default for a net11.0+ target, and
+ a second is available as an opt-in:
- runtime-async=on Runtime Async V2. .NET 11 no longer needs
- EnablePreviewFeatures for it, and the
- runtime libraries themselves ship built with
- it, so an application built without it runs
- against libraries that did opt in.
+ runtime-async=on Runtime Async V2. Off by default: it
+ changes how every await compiles, so it is
+ opted into with PurviewRuntimeAsync=true
+ (or UseRuntimeAsync=true), never assumed.
- updated-memory-safety-rules
+ updated-memory-safety-rules (default)
The C# 15 memory-safety model: 'unsafe'
marks requires-unsafe APIs and the caller
must acknowledge the access. Independent of
@@ -43,7 +43,11 @@
- true
+
+ $(UseRuntimeAsync)
+ false
2
<_PurviewMemorySafetyRulesNormalized>$([System.String]::Copy('$(MemorySafetyRules)').Trim().ToLowerInvariant())
diff --git a/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs b/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs
index 1811df3..cdf294b 100644
--- a/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs
@@ -15,7 +15,7 @@ namespace Purview.BuildSdk;
sealed class Dotnet11FeaturesTests
{
[Test]
- public async Task Net11Target_EnablesRuntimeAsyncAndUpdatedMemorySafetyRules(CancellationToken cancellationToken)
+ public async Task Net11Target_EnablesUpdatedMemorySafetyRulesButNotRuntimeAsync(CancellationToken cancellationToken)
{
using var h = await ProjectHarness.CreateAsync(
"MyLibrary",
@@ -30,8 +30,8 @@ public async Task Net11Target_EnablesRuntimeAsyncAndUpdatedMemorySafetyRules(Can
await Assert
.That(eval.Properties["Features"])
- .Contains("runtime-async=on")
- .Because("Runtime Async V2 is the .NET 11 default and no longer needs EnablePreviewFeatures");
+ .DoesNotContain("runtime-async=on")
+ .Because("Runtime Async is opt-in: it changes how every await compiles");
await Assert
.That(eval.Properties["Features"])
.Contains("updated-memory-safety-rules")
@@ -75,37 +75,53 @@ public async Task Net11Target_KeepsFeaturesTheProjectAlreadySet(CancellationToke
var features = await h.GetPropertyAsync("Features", cancellationToken);
await Assert.That(features).Contains("project-feature=on");
- await Assert.That(features).Contains("runtime-async=on");
await Assert.That(features).Contains("updated-memory-safety-rules");
}
[Test]
- public async Task RuntimeAsync_CanBeDisabledPerProject(CancellationToken cancellationToken)
+ public async Task RuntimeAsync_IsOptedInByPurviewRuntimeAsync(CancellationToken cancellationToken)
{
using var h = await ProjectHarness.CreateAsync(
"MyLibrary",
targetFramework: "net11.0",
- extraProps: "false",
+ extraProps: "true",
cancellationToken: cancellationToken
);
var features = await h.GetPropertyAsync("Features", cancellationToken);
- await Assert.That(features).DoesNotContain("runtime-async=on");
+ await Assert.That(features).Contains("runtime-async=on");
await Assert.That(features).Contains("updated-memory-safety-rules");
}
///
- /// The documented opt-out is UseRuntimeAsync=false; honouring it even though the SDK's own
- /// switch is separate is what keeps a project compatible with a future .NET SDK that owns the name.
+ /// The .NET team's property name opts in as well, so a repository can use the name it already
+ /// knows rather than learn the SDK's own switch.
///
[Test]
- public async Task RuntimeAsync_HonoursTheDocumentedUseRuntimeAsyncOptOut(CancellationToken cancellationToken)
+ public async Task RuntimeAsync_IsOptedInByUseRuntimeAsync(CancellationToken cancellationToken)
{
using var h = await ProjectHarness.CreateAsync(
"MyLibrary",
targetFramework: "net11.0",
- extraProps: "false",
+ extraProps: "true",
+ cancellationToken: cancellationToken
+ );
+
+ await Assert.That(await h.GetPropertyAsync("Features", cancellationToken)).Contains("runtime-async=on");
+ }
+
+ ///
+ /// An explicit opt-out under either name wins over the other name's opt-in, so a repository can
+ /// disable Runtime Async without finding every place that enabled it.
+ ///
+ [Test]
+ public async Task RuntimeAsync_ExplicitOptOutWinsOverAnOptIn(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net11.0",
+ extraProps: "truefalse",
cancellationToken: cancellationToken
);
@@ -125,7 +141,7 @@ public async Task MemorySafetyTierZero_DisablesTheUpdatedRules(CancellationToken
var features = await h.GetPropertyAsync("Features", cancellationToken);
await Assert.That(features).DoesNotContain("updated-memory-safety-rules");
- await Assert.That(features).Contains("runtime-async=on");
+ await Assert.That(features).DoesNotContain("runtime-async=on");
}
///
diff --git a/templates/global.json b/templates/global.json
index 739d4fb..6846896 100644
--- a/templates/global.json
+++ b/templates/global.json
@@ -3,6 +3,6 @@
"runner": "Microsoft.Testing.Platform"
},
"msbuild-sdks": {
- "Purview.BuildSdk": "2.0.0"
+ "Purview.BuildSdk": "2.0.1"
}
}