diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index cafa6cb..f043030 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"csharpier": {
- "version": "1.3.0",
+ "version": "1.4.0",
"commands": [
"csharpier"
],
@@ -17,7 +17,7 @@
"rollForward": false
},
"purview.build": {
- "version": "0.8.2",
+ "version": "0.8.1",
"commands": [
"purview-build"
],
diff --git a/.config/purview-build.json b/.config/purview-build.json
index 732f2bd..97b2f99 100644
--- a/.config/purview-build.json
+++ b/.config/purview-build.json
@@ -23,6 +23,8 @@
"Sdk/Sdk.props",
"Sdk/Sdk.targets",
"Sdk/Targets/Blazor.targets",
+ "Sdk/Targets/Modern.targets",
+ "Sdk/Targets/ServerShared.targets",
"Sdk/Targets/VersionDetection.targets"
],
"purview.skills.testing": [
diff --git a/.editorconfig b/.editorconfig
index 138c5a4..a9b39cb 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,4 +1,4 @@
-# Purview.BuildSdk 1.3.2 - managed file, do not edit
+# Purview.BuildSdk 2.0.0 - 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/.gitignore b/.gitignore
index 23793e5..ac539eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,6 +49,7 @@ project.lock.json
project.fragment.lock.json
artifacts/
+*.rsp
*_i.c
*_p.c
*_i.h
@@ -60,6 +61,9 @@ artifacts/
*.pgc
*.pgd
*.rsp
+# The SDK bootstraps a managed response file at the repository root; it is tracked so the first build
+# in a fresh clone already runs MSBuild's multithreaded mode.
+!/Directory.Build.rsp
*.sbr
*.tlb
*.tli
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 323f4c3..8a49fbf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,35 @@ release summarise what the commits recorded rather than what was written at the
## [Unreleased]
+### Added
+
+- **`ServerShared` projects.** A `ServerShared*` project is referenced automatically by every
+ non-client-side project, and never by a WebAssembly client, `PurviewBrowserSafe` library, Razor
+ class library or `*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` project.
+ `IsClientSideProject` names the classification and can be set explicitly; `PRSHR0001` (client-side
+ reference) and `PRSHR0002` (Shared reference) catch a reference that would still reach a browser.
+ Opt the automatic reference out with `PurviewAutoServerSharedReference=false`.
+- **.NET 11 compiler opt-ins.** A `net11.0`+ target compiles with Runtime Async
+ (`runtime-async=on`) and the C# 15 updated memory-safety rules
+ (`MemorySafetyRules=2`), gated per-TFM so the `net10.0` leg is unchanged. `AllowUnsafeBlocks` is
+ left at the compiler default (`false`). Opt out with `PurviewRuntimeAsync=false`,
+ `UseRuntimeAsync=false`, or `MemorySafetyRules=0`; an unrecognised tier fails with `PRCFG0001`.
+- **Multithreaded builds.** A managed `Directory.Build.rsp` at the repository root enables MSBuild's
+ `-mt` on .NET 11, bootstrapped like `.editorconfig` and `global.json` and refreshed only when its
+ marker line is present. `PurviewMultiThreadedBuild=false` rewrites it to `-mt:false`.
+
+### Changed
+
+- **The default target frameworks are now `net10.0;net11.0`.** `Current` — the set that applies when a
+ project selects none — resolves to the current LTS plus the latest release, so a project that declares
+ neither `TargetFramework` nor `TargetFrameworks` now multi-targets. This is a breaking change: pin
+ `PurviewTargetFrameworksCurrent`, select another set, or declare the TFM explicitly to keep the previous
+ single-target output. `Supported` still names the same pair so a repository can pin the meaning rather
+ than depend on where `Current` moves next.
+- **`LangVersion` defaults to `latest` (C# 15)** instead of `preview`. C# 15 is the latest shipped
+ language version and includes union declarations; `preview` remains available per project when a
+ repository deliberately wants unshipped features. Roslyn components keep `LangVersion=latest`.
+
## [1.3.2] - 2026-11-09
### Changed
diff --git a/Directory.Build.rsp b/Directory.Build.rsp
new file mode 100644
index 0000000..f7123f5
--- /dev/null
+++ b/Directory.Build.rsp
@@ -0,0 +1,4 @@
+# Purview.BuildSdk - managed file
+# Managed by Purview.BuildSdk. Delete the marker line to take ownership of this file.
+-mt
+-nr:false
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 7a2f40c..5259269 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,6 +1,6 @@
- 1.76.0
+ 1.83.0
5.9.0
diff --git a/README.md b/README.md
index ff272a6..3adc17d 100644
--- a/README.md
+++ b/README.md
@@ -12,18 +12,20 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def
| Feature | Detail |
| -- | -- |
-| **Project type detection** | `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsContainerProject`, `IsWebSdkProject`, `IsAspireHostProject`, `IsBlazorWebAssemblyProject`, `IsBffProject`, … |
+| **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)) |
+| **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` TFM (overridable), `LangVersion=preview`, `Nullable=enable`, `ImplicitUsings=enable`, deterministic builds |
+| **C# defaults** | `net10.0;net11.0` TFMs (overridable), `LangVersion=latest` (C# 15), `Nullable=enable`, `ImplicitUsings=enable`, deterministic builds |
| **Code style** | `.editorconfig` baked into the package, applied via `EditorConfigFilePath`, and auto-bootstrapped to repo root if missing; `EnforceCodeStyleInBuild=true`, `EnableNETAnalyzers=true`, `AnalysisLevel=latest`, `AnalysisMode=All`. Style policy: `dotnet_style_require_accessibility_modifiers=omit_if_default`, private instance fields named `_camelCase`, and `IDE0040`/`CA1515`/`CA1852`/`CA1034` at warning or above, enforced by `ValidatePurviewStylePolicy` (opt out with `DisablePurviewStylePolicyValidation=true`) |
| **NuGet packaging** | `AssemblyName`/`PackageId` default to the fully evaluated `RootNamespace`; packable projects get `GenerateDocumentationFile=true`, `PublishRepositoryUrl=true`, `IncludeSymbols=true`, `SymbolPackageFormat=snupkg`, `EmbedUntrackedSources=true`, and portable PDBs delivered via `.snupkg` (not the `.nupkg`) |
-| **Repo bootstrap** | Missing repo-root `.editorconfig` and `global.json` are auto-copied/created by default (disable via `DisableAutoCopySdkFiles=true`) |
+| **Repo bootstrap** | Missing repo-root `.editorconfig`, `global.json` and `Directory.Build.rsp` are auto-created by default (disable via `DisableAutoCopySdkFiles=true`) |
| **CI detection** | `ContinuousIntegrationBuild` set automatically when `CI`, `GITHUB_ACTIONS`, or `TF_BUILD` env vars are present |
| **SourceLink** | `Microsoft.SourceLink.GitHub` added to all packable projects (configurable via `SourceLinkPackageName`) |
| **Purview Telemetry** | `Purview.Telemetry.SourceGenerator` + `Microsoft.Extensions.Telemetry.Abstractions` added by default (opt-out) |
| **Assembly info** | Auto-generated `static partial class AssemblyInfo` with `RootNamespace`, `Version`, `Company`, etc., plus an embedded `Microsoft.CodeAnalysis.EmbeddedAttribute` (can be excluded via `PURVIEW_SDK_EXCLUDE_EMBEDDED`). |
| **InternalsVisibleTo** | Generated for all `TestType` variants and shared testing projects, using the resolved `$(AssemblyName)` so explicit, generated, and default naming are all handled |
-| **Namespace management** | `NamespacePrefix.ProjectName` pattern with suffix stripping (`.Core`, `.Shared`, `.EF`, …) |
+| **Namespace management** | `NamespacePrefix.ProjectName` pattern with suffix stripping (`.Core`, `.Shared`, `.ServerShared`, `.EF`, …) |
| **Testing framework** | `TestingFramework`: **TUnit** (default), `Xunit`, or `None` |
| **Mocking provider** | `SubstituteFramework`: **TUnitMocks** (default), `NSubstitute`, or `None` |
| **Test data provider** | `TestDataFramework`: **Bogus** (default) or `None` |
@@ -42,7 +44,7 @@ A reusable MSBuild SDK NuGet package that delivers standardised .NET project def
"runner": "Microsoft.Testing.Platform"
},
"msbuild-sdks": {
- "Purview.BuildSdk": "1.3.2"
+ "Purview.BuildSdk": "2.0.0"
}
}
```
@@ -201,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 1.3.2 - managed file, do not edit
+# Purview.BuildSdk 2.0.0 - managed file, do not edit
```
That line is how the SDK recognises a file **it** wrote. When the SDK version changes, a file carrying
@@ -363,7 +365,7 @@ Version detection logging is disabled by default. Set `VersionDetectionLogEnable
| `PurviewTestContextNoWarn` | `CA1002;CA1012;CA1034;CA1047;CA1050;CA1051;CA1062;CA1064;CA1515;CA1707` | Semicolon-fenced rule set exempted in test and shared-testing projects, so test classes stay public, `Method_Scenario_Expectation` names keep their underscores, and helpers need no null guards. Override before the SDK import to narrow or extend it. |
| `DisablePurviewTestContextRuleSet` | `false` | Set to `true` to make test and shared-testing projects enforce the production API-surface rules as well (the strict style contract applies either way). |
| `PurviewSharedTestingOutputType` | `Library` | Output type forced on `IsSharedTestingProject` projects. `Library` (default) keeps them helper libraries - with `IsTestProject`/`IsTestingPlatformApplication` cleared - which also keeps their fixtures out of `CA1515`'s Exe-only scope. Set to `Exe` before the SDK import to keep the test packages' executable/test-host shape instead. |
-| `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. |
+| `TargetFramework` | `net10.0;net11.0` | Override the default TFMs per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. |
| `IsRoslynComponent` | `false` | When explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, compiler-generated output under the intermediate directory, no dependency file, telemetry exclusion, and package build output. Pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true`. Roslyn development dependencies (`Microsoft.CodeAnalysis.*`, `Microsoft.CodeAnalysis.Analyzers`) default to `PrivateAssets="all"`. |
| `IsRoslynComponentOnly` | `true` for Roslyn components | Produces an analyzer-only package: `IncludeBuildOutput=false`, `IncludeSymbols=false`, and the analyzer assembly and portable PDB are packed under `analyzers/dotnet/cs/`. Set to `false` for a dual-role Roslyn component that uses normal library symbol packaging. |
| `PackProjectReferencedSourceGenerators` | `true` | Automatically packs analyzer `ProjectReference` outputs and their runtime dependencies under `analyzers/dotnet/cs/`. Set to `false` to opt out; set `Pack="false"` on an individual reference to exclude only that generator. |
@@ -375,6 +377,12 @@ Version detection logging is disabled by default. Set `VersionDetectionLogEnable
| `DisableGenerateAssemblyIdentifierClass` | `false` (`true` for test and shared-testing projects) | Set to `true` to disable the generated assembly identifier marker class. |
| `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. |
+| `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`. |
+| `PurviewServerSharedReferenceCheckAsError` | `false` | Makes `PRSHR0001`/`PRSHR0002` errors instead of warnings. |
### Packable project defaults
@@ -407,6 +415,9 @@ Non-packable projects (including web applications) default `WarnOnPackingNonPack
| `RepositoryEditorConfigFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `.editorconfig`. |
| `BootstrapGlobalJsonToRepoRoot` | `true` | Creates a `global.json` at the repository root when missing. |
| `RepositoryGlobalJsonFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `global.json`. |
+| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Creates a managed `Directory.Build.rsp` at the repository root that enables MSBuild's multithreaded build (`-mt`) on .NET 11. |
+| `RepositoryDirectoryBuildRspFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `Directory.Build.rsp`. |
+| `PurviewMultiThreadedBuild` | `true` | When `false`, a managed `Directory.Build.rsp` is rewritten to `-mt:false` so the mode is actually off. |
| `PurviewBuildSdkVersionForGlobalJson` | *(restored package version, else the version it was packed as)* | Version written to the `msbuild-sdks.Purview.BuildSdk` entry in a bootstrapped `global.json`. |
| `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing` never touches an existing file, `Always` overwrites it, `WarnOnDrift` reports a file that differs from the SDK-provided one, and `Never` skips bootstrapping. |
| `PurviewRepoBootstrapCopyRetries` | `3` | Write attempts before a bootstrap write failure is reported. |
@@ -565,6 +576,81 @@ The SDK now exports its properties via `CompilerVisibleProperty`, so analyzers a
---
+## .NET 11 features
+
+.NET 11 is the baseline this SDK targets. The switches below are applied only to a `net11.0` (or
+later) target, so the `net10.0` leg of the default `net10.0;net11.0` set is unaffected.
+
+| 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`. |
+| 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. |
+
+`MemorySafetyRules` tiers: `0` (off), `1` (preview pointer syntax, sets `LangVersion=preview` unless the
+project named its own version), `2` (updated rules, the default). Anything else fails with `PRCFG0001`.
+
+### Evaluated and deliberately not defaulted
+
+These improve a specific workload but would change behaviour or cost for every project, so they stay
+opt-in:
+
+- **Server GC / `TieredPGO` / ReadyToRun** — the runtime and SDK already choose sensible defaults per
+ app type; a blanket `ServerGarbageCollection=true` is wrong for desktop apps and tests.
+- **Native AOT and `PublishAot`** — a publish property, not a build default, and it changes the
+ artifact and the libraries an app may use.
+- **`UseArtifactsOutput`** — a much faster, cache-friendly layout, but a breaking change to every
+ output path a repository may reference.
+- **`EnablePackageValidation`** — needs a baseline package, so it is a per-package decision.
+- **`NuGetAuditMode=all`** — a good security default, but it turns transitive vulnerability warnings
+ on for every repository, which is a policy decision rather than a build default.
+- **`CheckForOverflowUnderflow`** — improves correctness at a measurable runtime cost; opt in where
+ arithmetic overflow is a real risk.
+
+The SDK does adopt the .NET 11 behaviour that needs no switch: `AnalysisLevel=latest` now resolves to
+the .NET 11 rule set, the MSBuild server is on by default in the SDK, and the default C# version is
+`latest` (C# 15).
+
+---
+
+## ServerShared
+
+`ServerShared` is the server-side counterpart of `Shared`. A project is recognised by the same
+name list shape (`ServerShared`, `ServerSharedFramework`, `ServerSharedInfrastructure`,
+`ServerSharedInfra`, `ServerSharedUtilities`, `ServerSharedUtils`, `ServerSharedLibrary`,
+`ServerSharedLib`, `ServerSharedHelpers`) and is referenced automatically by every project that
+cannot run in a browser.
+
+`Shared` is referenced by almost everything, which is right for helpers that run anywhere, but it
+leaks server-only code the moment a WebAssembly client reaches anything in that chain. So the
+automatic `ServerShared` reference is skipped for anything that might be downloaded:
+
+| Excluded | Why |
+| -- | -- |
+| `Microsoft.NET.Sdk.BlazorWebAssembly` | the client itself is downloaded |
+| `PurviewBrowserSafe=true` | by definition downloaded |
+| `Microsoft.NET.Sdk.Razor` | a WebAssembly client can reference a Razor class library, and the build cannot know in advance whether it will |
+| `*.Client`, `*.Browser`, `*.Wasm`, `*.WebAssembly`, `*.Components`, `Components` | the shape a Blazor Web App puts on the client |
+| `Shared` and `SharedTesting` | a `Shared` project is itself referenced widely, so inheriting the reference would defeat the guarantee |
+
+`IsClientSideProject=true`/`false` overrides the detection either way, including restoring the
+automatic reference to a server-only Razor class library (the conservative default would otherwise
+hide it).
+
+Two diagnostics protect the guarantee when a reference is deliberate:
+
+- `PRSHR0001` — a client-side project references a `ServerShared` project.
+- `PRSHR0002` — a `Shared` project references a `ServerShared` project.
+
+Both are warnings; `PurviewServerSharedReferenceCheckAsError=true` makes them errors and
+`DisablePurviewServerSharedReferenceCheck=true` suppresses them. `PurviewBrowserSafe="true"` on the
+`ProjectReference` is accepted as the explicit statement that the reference was judged safe, the same
+escape hatch `PRBLZ0001` uses. `PurviewAutoServerSharedReference=false` removes the automatic
+reference entirely.
+
+---
+
## Blazor WebAssembly and BFF
Every behaviour here applies only to the project type it names. A project that is not a WebAssembly
@@ -591,8 +677,8 @@ opts out). A host referencing a client under another name is resolved during the
| -- | -- |
| `IsPackable=false` (the project can still set `true`) | packing an app's `wwwroot` as a package nobody can consume |
| no container defaults, even with a `Dockerfile` | `PublishAot` on a client is a publish that fails |
-| no automatic `../Shared*` reference | a server-side library joining the browser download because of a folder name |
-| `PRBLZ0001` for each `ProjectReference` that does not set `PurviewBrowserSafe` | a library joining the download without anyone deciding it was safe there |
+| no automatic `../Shared*` or `../ServerShared*` reference | a server-side library joining the browser download because of a folder name |
+| `PRBLZ0001` for each `ProjectReference` that does not set `PurviewBrowserSafe`, and `PRSHR0001` for a `ServerShared` reference | a library joining the download without anyone deciding it was safe there |
Globalisation, trimming mode and the Blazor runtime settings are left to the Blazor SDK.
`DisablePurviewBlazorWebAssemblyDefaults=true` (before the SDK import) restores the generic
diff --git a/docs/wiki/Configuration-Reference.md b/docs/wiki/Configuration-Reference.md
index d8a8f05..0e56da2 100644
--- a/docs/wiki/Configuration-Reference.md
+++ b/docs/wiki/Configuration-Reference.md
@@ -24,7 +24,7 @@ See [Version Detection](Version-Detection.md) for the full resolution rules.
| `PurviewTestContextNoWarn` | `CA1002;CA1012;CA1034;CA1047;CA1050;CA1051;CA1062;CA1064;CA1515;CA1707` | Production API-surface rules exempted in test and shared-testing projects (they keep the strict style contract). Override before the SDK import to narrow or extend the list. |
| `DisablePurviewTestContextRuleSet` | `false` | Set to `true` to make test and shared-testing projects enforce the production API-surface rules as well. |
| `PurviewSharedTestingOutputType` | `Library` | Output type forced on `IsSharedTestingProject` projects; `Library` also clears `IsTestProject`/`IsTestingPlatformApplication`. Set to `Exe` before the SDK import to keep the test packages' executable/test-host shape. |
-| `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. To multi-target from a central, overridable definition rather than a hard-coded list, see [Target framework sets](#target-framework-sets). |
+| `TargetFramework` | `net10.0;net11.0` | Override the default TFMs per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. To multi-target from a central, overridable definition rather than a hard-coded list, see [Target framework sets](#target-framework-sets). |
| `DisableUnconditionalProjectDeclarationCheck` | `false` | Set to `true` to suppress `PurviewConditionedProjectDeclaration`. The SDK reads `TargetFramework`, `TargetFrameworks`, `IsRoslynComponent`, `IsRoslynComponentOnly`, `IsPackable` and `PurviewTargetFrameworkSet` from the project XML before the project body is evaluated, so it cannot evaluate a `Condition` on them and will not see the declaration — the SDK default applies instead. Declare those properties unconditionally, or set them in a file imported before this SDK. |
| `IsRoslynComponent` | `false` | When explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, compiler-generated output under the intermediate directory, no dependency file, symbol packaging (`IncludeSymbols=false` by default), telemetry exclusion, and package build output. Packable Roslyn components automatically pack the built analyzer assembly and its PDB into `analyzers/dotnet/cs/` (`PurviewPackAnalyzerPdb=true`; set `false` only when symbols are delivered another way — NuGet's `.snupkg` cannot host `analyzers/dotnet/cs` symbols). Pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true`. Roslyn development dependencies (`Microsoft.CodeAnalysis.*`, `Microsoft.CodeAnalysis.Analyzers`) default to `PrivateAssets="all"`. |
| `PackProjectReferencedSourceGenerators` | `true` | Automatically packs analyzer `ProjectReference` outputs and their runtime dependencies under `analyzers/dotnet/cs/`. Set to `false` to opt out; set `Pack="false"` on an individual reference to exclude only that generator. |
@@ -36,6 +36,12 @@ See [Version Detection](Version-Detection.md) for the full resolution rules.
| `DisableGenerateAssemblyIdentifierClass` | `false` (`true` for test and shared-testing projects) | Set to `true` to disable the generated assembly identifier marker class. See [Assembly identifier class](Assembly-Name-Generation.md#assembly-identifier-class). |
| `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. |
+| `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`. |
+| `PurviewServerSharedReferenceCheckAsError` | `false` | Reports `PRSHR0001`/`PRSHR0002` as errors. |
| `NamespaceRemoveSuffix` | *(built-in list)* | Item type listing the suffixes stripped from `RootNamespace`. Remove an entry **after** the `Sdk.props` import to keep that suffix in the namespace, e.g. ``. **Roslyn component names are not in the list** — `SourceGenerator`, `SourceGenerators`, `SourceGeneration`, `Generators`, `Analyzers`, `CodeFixers` and `CodeFixes` are part of a component's identity, and a generator, its analyzers and its code fixes are separate assemblies that each need their own namespace. Add one with `` if a repository really wants it collapsed. See [Namespace stripping](Assembly-Name-Generation.md#namespace-stripping). |
## Target framework sets
@@ -53,7 +59,7 @@ moves every consumer at once:
| Set | Resolves to | Use for |
| -- | -- | -- |
-| `Current` | `net10.0` | The default. A single target, stated explicitly. |
+| `Current` | `net10.0;net11.0` | The default. The current LTS plus the latest release. |
| `Latest` | `net11.0` | Newest release only. |
| `Supported` | `net10.0;net11.0` | A package that should work on anything still in support. |
| `Broad` | `net8.0;net9.0;net10.0` | Every shipped release — widest reach without committing to the newest major while it is pre-GA. |
@@ -65,10 +71,9 @@ package should not imply a commitment to them. Choose `Broad` to keep them.
`Broad` and `All` differ by one real product decision — whether the package commits to the newest
release — so they are separate names rather than one set that silently widens every consumer of it.
-A set is **always** in play: `Current` applies when nothing is selected, which resolves to the same
-single TFM the SDK used to hard-code. The out-of-box result is therefore unchanged — but the value is
-named and overridable in one place, so moving a repository (or every repository) between runtimes is a
-property change rather than an edit in each project.
+A set is **always** in play: `Current` applies when nothing is selected, and resolves to every release
+still in support. The value is named and overridable in one place, so moving a repository (or every
+repository) between runtimes is a property change rather than an edit in each project.
Overrides are layered, narrowest first:
@@ -86,8 +91,8 @@ collapse the project to a single target and break every multi-targeting consumer
| Property | Default | Description |
| -- | -- | -- |
-| `PurviewTargetFrameworkSet` | `Current` | `Current`, `Latest`, `Supported`, `Broad` or `All`. An unrecognised name fails the build with `PurviewInvalidTargetFrameworkSet` rather than silently falling back to the single-TFM default. A single-entry set resolves to `TargetFramework`, so it does not pay for an outer multi-targeting build. |
-| `PurviewTargetFrameworksCurrent` | `net10.0` | Redefines the `Current` set. |
+| `PurviewTargetFrameworkSet` | `Current` | `Current`, `Latest`, `Supported`, `Broad` or `All`. An unrecognised name fails the build with `PurviewInvalidTargetFrameworkSet` rather than silently falling back to the default set. A single-entry set resolves to `TargetFramework`, so it does not pay for an outer multi-targeting build. |
+| `PurviewTargetFrameworksCurrent` | `net10.0;net11.0` | Redefines the `Current` set. |
| `PurviewTargetFrameworksLatest` | `net11.0` | Redefines the `Latest` set. |
| `PurviewTargetFrameworksSupported` | `net10.0;net11.0` | Redefines the `Supported` set. |
| `PurviewTargetFrameworksBroad` | `net8.0;net9.0;net10.0` | Redefines the `Broad` set. |
@@ -152,6 +157,9 @@ operations skip them silently.
| `RepositoryEditorConfigFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `.editorconfig`. |
| `BootstrapGlobalJsonToRepoRoot` | `true` | Creates a `global.json` at the repository root when missing. |
| `RepositoryGlobalJsonFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `global.json`. |
+| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Creates a managed `Directory.Build.rsp` at the repository root that enables MSBuild's multithreaded build (`-mt` plus `-nr:false`) on .NET 11. Set to `false` to stop bootstrapping it. |
+| `RepositoryDirectoryBuildRspFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `Directory.Build.rsp`. |
+| `PurviewMultiThreadedBuild` | `true` | When `false`, a managed `Directory.Build.rsp` is rewritten to `-mt:false`, so opting out cannot leave the mode enabled. |
| `PurviewBuildSdkVersionForGlobalJson` | *(restored package version, else the version it was packed as)* | Version written to the `msbuild-sdks.Purview.BuildSdk` entry in a bootstrapped `global.json`. |
| `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing` never touches an existing file, `Always` overwrites it, `WarnOnDrift` reports that an existing file differs from the SDK-provided one, and `Never` skips bootstrapping entirely. |
| `PurviewRepoBootstrapCopyRetries` | `3` | Copy attempts before a bootstrap write failure is reported. |
@@ -160,9 +168,46 @@ operations skip them silently.
| `PurviewSuppressCopyRetryWarnings` | `true` | Demotes MSB3026 copy retry notices to messages. Set to `false` to see every retry attempt. |
Bootstrap writes are staged into a temporary file and renamed into place, so editors and tools never
-observe a partially written `.editorconfig` or `global.json`. Because every project in a solution runs
-the same bootstrapping targets, a lost race between parallel projects is a no-op: an existing file is
-treated as success rather than a copy failure.
+observe a partially written `.editorconfig`, `global.json` or `Directory.Build.rsp`. Because every
+project in a solution runs the same bootstrapping targets, a lost race between parallel projects is a
+no-op: an existing file is treated as success rather than a copy failure.
+
+The `Directory.Build.rsp` is SDK-managed: its first line carries `# Purview.BuildSdk - managed file`,
+and only a file carrying that marker is refreshed. It contains `-mt` and `-nr:false`. `-mt` is a build
+host switch, so a response file is the only way a project can turn it on; `-nr:false` avoids reusing a
+node left by a non-`-mt` build, which can cache a `RoslynCodeTaskFactory` task with no assembly path
+and fail the first `-mt` build (`dotnet/msbuild#15213`). `-mt` still uses the MSBuild server (short-lived)
+for Server GC.
+
+## .NET 11 features
+
+A `net11.0`+ target compiles with these switches; the `net10.0` leg of a multi-targeted project is
+unchanged.
+
+| Property | Applied | Notes |
+| -- | -- | -- |
+| `PurviewRuntimeAsync` | `true` | `runtime-async=on`. `UseRuntimeAsync=false` also opts out. |
+| `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. |
+
+Server GC, `TieredPGO`, ReadyToRun, Native AOT, `UseArtifactsOutput`, `EnablePackageValidation`,
+`NuGetAuditMode=all` and `CheckForOverflowUnderflow` were evaluated and deliberately left opt-in:
+each changes behaviour or cost for every project rather than improving one workload. See
+[.NET 11 features](https://github.com/purview-dev/build-sdk#net-11-features) in the README.
+
+## ServerShared
+
+A `ServerShared*` project is referenced automatically by every project that cannot run in a browser.
+It is skipped for a WebAssembly client, a `PurviewBrowserSafe` library, a Razor class library, a
+`*.Client`/`*.Browser`/`*.Wasm`/`*.WebAssembly`/`*.Components` project, and for `Shared`/`SharedTesting`
+projects (which are themselves referenced widely). `IsClientSideProject` can override the classification
+either way.
+
+`PRSHR0001` reports a client-side project that references a `ServerShared` project; `PRSHR0002`
+reports a `Shared` project that does. Both are warnings by default, become errors with
+`PurviewServerSharedReferenceCheckAsError=true`, and are suppressed by
+`DisablePurviewServerSharedReferenceCheck=true`. `PurviewBrowserSafe="true"` on the `ProjectReference`
+records that the reference was judged safe.
## Sdk folder packaging
@@ -250,13 +295,23 @@ read them through `build_property.`:
| `IsAspireHostProject` | True when SDK starts with `Aspire.Sdk.Host`. |
| `IsCLIProject` | True when the project is a CLI project. |
| `IsSharedProject` | True when the project is a shared project. |
+| `IsServerSharedProject` | True for a `ServerShared*` project, which only non-client-side projects reference automatically. |
+| `IsClientSideProject` | True when the project's assemblies are downloaded to a browser (WebAssembly client, browser-safe library, Razor class library, or a client-side name segment). |
+| `PurviewAutoServerSharedReference` | When `true` (default), adds a sibling `ServerShared` reference to a non-client-side project. |
+| `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`. |
+| `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`. |
| `BootstrapEditorConfigToRepoRoot` | When `true` (default), copies the SDK `.editorconfig` to `RepositoryEditorConfigFilePath` if missing. |
| `RepositoryGlobalJsonFilePath` | Destination path for bootstrapping a physical repo-level `global.json`. |
| `BootstrapGlobalJsonToRepoRoot` | When `true` (default), creates `global.json` at `RepositoryGlobalJsonFilePath` if missing. |
+| `RepositoryDirectoryBuildRspFilePath` | Destination path for bootstrapping a physical repo-level `Directory.Build.rsp`. |
+| `BootstrapMultiThreadedBuildRspToRepoRoot` | When `true` (default), creates a managed `Directory.Build.rsp` enabling `-mt`. |
| `PurviewBuildSdkVersionForGlobalJson` | Version used for `msbuild-sdks.Purview.BuildSdk` when bootstrapping `global.json`. |
-| `DisableAutoCopySdkFiles` | When `true`, disables SDK auto-copy/bootstrap for repo files (`.editorconfig`, `global.json`). |
+| `DisableAutoCopySdkFiles` | When `true`, disables SDK auto-copy/bootstrap for repo files (`.editorconfig`, `global.json`, `Directory.Build.rsp`). |
| `CurrentYear` | Current year used in generated assembly metadata. |
| `AutoGeneratedAssemblyInfoFile` | File name of the generated AssemblyInfo source file (written under the intermediate `Properties/` folder). |
| `AutoGeneratedAssemblyIdentifierFile` | File name of the generated assembly identifier source file (written under the intermediate `Properties/` folder). |
diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md
index 017ed63..1b3c797 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": "1.3.2"
+ "Purview.BuildSdk": "2.0.0"
}
}
```
diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md
index 2e3bfe5..7f48b2f 100644
--- a/docs/wiki/Home.md
+++ b/docs/wiki/Home.md
@@ -34,8 +34,8 @@ repo root inherits everything automatically.
`.csproj` filename, the `Sdk` attribute, and on-disk markers.
- **Engineering conventions** — repository layout, project naming, namespace identity, test readability,
and the default boundaries between source, test, and shared test-support projects.
-- **C# defaults** — `net10.0` TFM (overridable, `netstandard2.0` for Roslyn components),
- `LangVersion=preview` (`latest` for Roslyn components), `Nullable=enable`, `ImplicitUsings=enable`,
+- **C# defaults** — `net10.0;net11.0` TFMs (overridable, `netstandard2.0` for Roslyn components),
+ `LangVersion=latest` (C# 15, including unions), `Nullable=enable`, `ImplicitUsings=enable`,
deterministic builds, `ManagePackageVersionsCentrally=true`.
- **Code style** — an `.editorconfig` baked into the package, applied via `EditorConfigFilePath`, and
auto-bootstrapped to the repo root if missing; `EnforceCodeStyleInBuild=true`,
@@ -65,7 +65,7 @@ repo root inherits everything automatically.
## Requirements
-- .NET SDK 10.0 or later (the default TFM for source projects is `net10.0`).
+- .NET SDK 11.0 or later (the default TFMs for source projects are `net10.0` and `net11.0`).
- A `NamespacePrefix` set before the SDK import.
- A `Directory.Packages.props` at the repo root when Central Package Management is used (the SDK sets
`ManagePackageVersionsCentrally=true`).
diff --git a/docs/wiki/Project-Type-Detection.md b/docs/wiki/Project-Type-Detection.md
index c0fadf7..cde3e5f 100644
--- a/docs/wiki/Project-Type-Detection.md
+++ b/docs/wiki/Project-Type-Detection.md
@@ -15,6 +15,8 @@ wiki.
| `IsTestProject` | Project name matches `^.*\.([\w|_]\w*)Tests?$` — a target name, a dot, then any word before `Test`/`Tests`. The captured word becomes `TestingType`. |
| `IsSharedTestingProject` | Project name is one of `SharedTestingFramework`, `SharedTestingInfrastructure`, `SharedTestingInfra`, `SharedTestingUtilities`, `SharedTestingUtils`, `SharedTestingLibrary`, `SharedTestingLib`, `SharedTestingHelpers`. |
| `IsSharedProject` | Project name is one of `Shared`, `SharedFramework`, `SharedInfrastructure`, `SharedInfra`, `SharedUtilities`, `SharedUtils`, `SharedLibrary`, `SharedLib`, `SharedHelpers`. |
+| `IsServerSharedProject` | Project name is one of `ServerShared`, `ServerSharedFramework`, `ServerSharedInfrastructure`, `ServerSharedInfra`, `ServerSharedUtilities`, `ServerSharedUtils`, `ServerSharedLibrary`, `ServerSharedLib`, `ServerSharedHelpers`. |
+| `IsClientSideProject` | A WebAssembly client, a `PurviewBrowserSafe` library, a Razor class library, or a project named `*.Client`, `*.ClientShared`, `*.Browser`, `*.Wasm`, `*.WebAssembly`, `*.Components`, `Components`, or `ClientShared`. An explicit value overrides the detection. |
| `IsContainerProject` | A `Dockerfile`, `dockerfile`, or `Dockerfile.dev` exists in the project directory. |
| `IsSdkProject` | An `Sdk` value is parsed from the ``/`` element. |
| `IsWebSdkProject` | `SdkProjectName` is `Microsoft.NET.Sdk.Web`. |
@@ -87,6 +89,16 @@ The SDK ships the following full `TestType` set, in addition to the standard `Un
(`../Shared*/Shared*.csproj`); test projects automatically reference
`../SharedTesting*/SharedTesting*.csproj`.
+### ServerShared projects (`IsServerSharedProject=true`)
+
+- Referenced automatically by every non-client-side project via wildcard
+ (`../ServerShared*/ServerShared*.csproj`).
+- Never referenced automatically by a WebAssembly client, a `PurviewBrowserSafe` library, a Razor class
+ library, a project named with a client-side segment, or another `Shared`/`SharedTesting` project.
+ `IsClientSideProject` overrides the classification either way.
+- `PRSHR0001` reports a client-side project that references one; `PRSHR0002` reports a `Shared`
+ project that does. `PurviewBrowserSafe="true"` on the reference records an accepted decision.
+
### Container projects
- `InvariantGlobalization=true`, `PublishAot=true`, `DockerDefaultTargetOS=Linux`,
diff --git a/docs/wiki/Repository-Bootstrap.md b/docs/wiki/Repository-Bootstrap.md
index 8348e36..5cc07cd 100644
--- a/docs/wiki/Repository-Bootstrap.md
+++ b/docs/wiki/Repository-Bootstrap.md
@@ -66,9 +66,41 @@ file, and honours `PurviewRepoBootstrapMode`:
| `DisableAutoCopySdkFiles` | `false` | Master switch that disables repo-level SDK file bootstrapping. |
| `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing`, `Always`, `WarnOnDrift` or `Never`. |
+## `Directory.Build.rsp` bootstrapping
+
+`-mt` is an MSBuild **host** switch, so no project property can enable it: the host reads its switches
+before evaluation. A `Directory.Build.rsp` in any ancestor of a project is the one file the CLI reads
+that can, so the SDK writes one at the repository root:
+
+```text
+# Purview.BuildSdk - managed file
+# Managed by Purview.BuildSdk. Delete the marker line to take ownership of this file.
+-mt
+-nr:false
+```
+
+`-nr:false` is deliberate. A node left by a non-`-mt` build can hold a cached `RoslynCodeTaskFactory`
+task with no assembly path, and the first `-mt` build that reuses it fails with `MSB0001`
+([dotnet/msbuild#15213](https://github.com/dotnet/msbuild/issues/15213)) — which is exactly the
+transition a repository makes when this file first appears. A short-lived server still provides the
+Server GC `-mt` expects. `-mt` requires .NET 11; the bootstrap is skipped on an older SDK.
+
+The file is SDK-managed: a destination carrying the marker line is refreshed, one without it belongs
+to the developer and is never touched. `PurviewMultiThreadedBuild=false` rewrites it to `-mt:false`
+rather than deleting it, so opting out cannot leave a stale copy enabling the mode.
+
+Like the other bootstraps it writes atomically, retries quietly, and honours `PurviewRepoBootstrapMode`:
+
+| Property | Default | Description |
+| -- | -- | -- |
+| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Creates the `Directory.Build.rsp` when missing. |
+| `RepositoryDirectoryBuildRspFilePath` | *(auto-detected)* | Override the destination path. |
+| `PurviewMultiThreadedBuild` | `true` | `false` writes `-mt:false` instead of `-mt`. |
+| `DisableAutoCopySdkFiles` | `false` | Master switch that disables repo-level SDK file bootstrapping. |
+
## Retry and failure behaviour
-Both bootstraps share the same retry contract:
+All bootstraps share the same retry contract:
| Property | Default | Description |
| -- | -- | -- |
@@ -82,11 +114,11 @@ error by default, including the destination path and the underlying OS error.
## Repository root discovery
-Both bootstrap targets locate the repository root by:
+All bootstrap targets locate the repository root by:
1. Running `git rev-parse --show-toplevel` from the `Directory.Build.props` directory.
2. Falling back to probing upward for a `.git` marker.
3. Finally falling back to the `Directory.Build.props` directory itself.
-Set the `RepositoryEditorConfigFilePath` / `RepositoryGlobalJsonFilePath` properties explicitly to
-override discovery.
\ No newline at end of file
+Set the `RepositoryEditorConfigFilePath`, `RepositoryGlobalJsonFilePath` or
+`RepositoryDirectoryBuildRspFilePath` property explicitly to override discovery.
\ No newline at end of file
diff --git a/global.json b/global.json
index dcef14c..b03f93d 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,7 @@
{
- "test": {
- "runner": "Microsoft.Testing.Platform"
- }
-}
\ No newline at end of file
+ "sdk": { "version": "11.0.100" },
+
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
diff --git a/package.json b/package.json
index 71d3a7e..e361a6d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "build-sdk",
- "version": "1.3.2",
+ "version": "2.0.0",
"homepage": "https://purview.dev/projects/build-sdk/",
"bugs": {
"url": "https://github.com/purview-dev/build-sdk/issues"
@@ -9,4 +9,4 @@
"type": "git",
"url": "git+https://github.com/purview-dev/build-sdk.git"
}
-}
\ No newline at end of file
+}
diff --git a/skills/purview-buildsdk-configuration/SKILL.md b/skills/purview-buildsdk-configuration/SKILL.md
index e081eac..b540704 100644
--- a/skills/purview-buildsdk-configuration/SKILL.md
+++ b/skills/purview-buildsdk-configuration/SKILL.md
@@ -41,7 +41,7 @@ in `Directory.Build.props` unless the setting is deliberately project-specific.
| `TestingFramework` | `TUnit` | `TUnit`, `Xunit` or `None` |
| `SubstituteFramework` | `TUnitMocks` | `TUnitMocks`, `NSubstitute` or `None` |
| `TestDataFramework` | `Bogus` | `Bogus` or `None` |
-| `TargetFramework` | `net10.0` | `netstandard2.0` when `IsRoslynComponent=true` |
+| `TargetFramework` | `net10.0;net11.0` | `netstandard2.0` when `IsRoslynComponent=true` |
| `IsPackable` | `false` | Opts a project into the packable defaults |
| `UsePackageJsonVersion` | `true` | `true`, `false` or `Strict` — version from `package.json` |
| `ExcludePurviewTelemetry` | `false` | Drops the telemetry source generator |
@@ -50,6 +50,11 @@ in `Directory.Build.props` unless the setting is deliberately project-specific.
| `PurviewBrowserSafe` | *(unset)* | On a class library a WebAssembly client downloads: trim, AOT and browser-unsupported APIs fail the build |
| `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`+ |
+| `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` |
The full set, grouped by area and with the behaviour each one changes, is in
[references/properties.md](references/properties.md). Read it rather than guessing a property name —
@@ -89,11 +94,15 @@ is the SDK's answer, and it stays correct when the rules change. The exported se
## Repository bootstrap
-The SDK bootstraps two repository-wide files from the package: `.editorconfig` and `global.json`.
+The SDK bootstraps three repository-wide files from the package: `.editorconfig`, `global.json` and
+`Directory.Build.rsp`.
- `PurviewRepoBootstrapMode` — `IfMissing` (default), `Always`, `WarnOnDrift`, `Never`
- `DisableAutoCopySdkFiles` — the master off switch
- `PurviewRepoBootstrapCopyRetries` / `...RetryDelayMilliseconds` / `...FailureAsError`
+- `BootstrapMultiThreadedBuildRspToRepoRoot` / `PurviewMultiThreadedBuild` — the response file that
+ enables MSBuild `-mt` (and `-nr:false`, which avoids a `RoslynCodeTaskFactory` cache left by a
+ non-`-mt` build)
Every project in a solution writes those same destinations, so the write is race-tolerant by
design: an existing file is never overwritten unless asked, each write is staged into a temporary
diff --git a/skills/purview-buildsdk-configuration/references/properties.md b/skills/purview-buildsdk-configuration/references/properties.md
index 21ddc7f..82f84d7 100644
--- a/skills/purview-buildsdk-configuration/references/properties.md
+++ b/skills/purview-buildsdk-configuration/references/properties.md
@@ -21,7 +21,7 @@ consuming project supplies nothing.
| Property | Default | Behaviour |
| -- | -- | -- |
-| `TargetFramework` | `net10.0` | Applied when neither `TargetFramework` nor `TargetFrameworks` is set. A project that explicitly declares `IsRoslynComponent=true` defaults to `netstandard2.0` |
+| `TargetFramework` | `net10.0;net11.0` | Applied when neither `TargetFramework` nor `TargetFrameworks` is set. A project that explicitly declares `IsRoslynComponent=true` defaults to `netstandard2.0` |
| `IsRoslynComponent` | *(unset)* | When explicitly `true`: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, no dependency file, telemetry exclusion, and `PrivateAssets=all` on `Microsoft.CodeAnalysis.*` references |
| `IsRoslynComponentOnly` | `true` for Roslyn components | Analyzer-only package: `IncludeBuildOutput=false`, `IncludeSymbols=false`, portable PDB packed beside the analyzer under `analyzers/dotnet/cs/`. Set `false` for a dual-role component using normal library symbol packaging |
| `DisableRoslynCompilerDefaultsValidation` | `false` | Opts out of `ValidateRoslynComponentCompilerSettings`, which fails the pack of a packable Roslyn component missing `LangVersion`, `Nullable`, `TreatWarningsAsErrors` or `EnforceExtendedAnalyzerRules` |
@@ -36,6 +36,19 @@ consuming project supplies nothing.
| `DisablePurviewTestContextRuleSet` | `false` | Makes test and shared-testing projects enforce the production API-surface rules too |
| `PurviewPolicyExemptNoWarn` | *(derived)* | The SDK-injected `NoWarn` entries `ValidatePurviewStylePolicy` accepts |
+## .NET 11 features and ServerShared
+
+| 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 |
+| `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 |
+| `PurviewAutoServerSharedReference` | `true` | Adds `../ServerShared*/ServerShared*.csproj` to a non-client-side project |
+| `PurviewServerSharedReferenceCheckAsError` | `false` | Makes `PRSHR0001`/`PRSHR0002` errors |
+| `DisablePurviewServerSharedReferenceCheck` | `false` | Turns `PRSHR0001`/`PRSHR0002` off |
+
## Testing
| Property | Default | Behaviour |
@@ -119,6 +132,9 @@ reference was judged browser-safe when it cannot say so itself.
| `RepositoryEditorConfigFilePath` | *(derived)* | Overrides the destination `.editorconfig` |
| `BootstrapGlobalJsonToRepoRoot` | `true` | |
| `RepositoryGlobalJsonFilePath` | *(derived)* | Overrides the destination `global.json` |
+| `BootstrapMultiThreadedBuildRspToRepoRoot` | `true` | Creates a managed `Directory.Build.rsp` enabling `-mt` |
+| `RepositoryDirectoryBuildRspFilePath` | *(derived)* | Overrides the destination `Directory.Build.rsp` |
+| `PurviewMultiThreadedBuild` | `true` | `false` rewrites a managed rsp to `-mt:false` |
| `PurviewBuildSdkVersionForGlobalJson` | restored package version, else the version it was packed as | |
| `PurviewRepoBootstrapCopyRetries` | `3` | |
| `PurviewRepoBootstrapCopyRetryDelayMilliseconds` | `500` | |
@@ -148,7 +164,11 @@ Exported to analyzers and generators as `build_property.`:
- `ExcludePurviewTelemetry`, `ExcludeMSTelemetryExtension`
- `EnableAssemblyNameGeneration`, `DisableAutoInternalsVisibleTo`, `DisableGenerateAssemblyInfoClass`,
`DisableGenerateAssemblyIdentifierClass`, `AssemblyIdentifierClassName`
-- `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsSharedProject`
+- `IsCSharpProject`, `IsTestProject`, `IsSharedTestingProject`, `IsSharedProject`,
+ `IsServerSharedProject`, `IsClientSideProject`
+- `PurviewAutoServerSharedReference`, `DisablePurviewServerSharedReferenceCheck`,
+ `PurviewServerSharedReferenceCheckAsError`
+- `MemorySafetyRules`, `PurviewRuntimeAsync`, `PurviewMultiThreadedBuild`
- `TestingType`, `TargetProjectName`
- `IsContainerProject`, `IsSdkProject`, `SdkProjectName`, `IsWebProject`, `IsWebSdkProject`,
`IsWorkerSdkProject`, `IsAspireHostProject`, `IsCLIProject`
diff --git a/skills/purview-buildsdk-project-detection/SKILL.md b/skills/purview-buildsdk-project-detection/SKILL.md
index 8573ca4..bd80b3f 100644
--- a/skills/purview-buildsdk-project-detection/SKILL.md
+++ b/skills/purview-buildsdk-project-detection/SKILL.md
@@ -1,6 +1,6 @@
---
name: purview-buildsdk-project-detection
-description: Explain why Purview.BuildSdk classified a project as test, shared, shared-testing, CLI, web, worker, Aspire host, container, Roslyn component, Blazor WebAssembly client, Razor class library, Blazor host or BFF, and why it added a package, a project reference, a namespace or an analyzer automatically. Use when SDK behaviour looks surprising, when a test project gets no runner or no InternalsVisibleTo, or when reasoning about RootNamespace, AssemblyName and TargetProjectName.
+description: Explain why Purview.BuildSdk classified a project as test, shared, shared-testing, ServerShared, client-side, CLI, web, worker, Aspire host, container, Roslyn component, Blazor WebAssembly client, Razor class library, Blazor host or BFF, and why it added a package, a project reference, a namespace or an analyzer automatically. Use when SDK behaviour looks surprising, when a test project gets no runner or no InternalsVisibleTo, or when reasoning about RootNamespace, AssemblyName and TargetProjectName.
license: MIT
metadata:
package: Purview.BuildSdk
@@ -25,7 +25,8 @@ dotnet msbuild -getProperty:PurviewLogicalProjectName -getProperty:Roo
The values worth inspecting first: `MSBuildProjectName`, `NamespacePrefix`,
`PurviewLogicalProjectName`, `RootNamespace`, `TestingType`, `TargetProjectName`, `SdkProjectName`,
-`IsTestProject`, `IsSharedProject`, `IsSharedTestingProject`, `IsContainerProject`, `IsCLIProject`,
+`IsTestProject`, `IsSharedProject`, `IsServerSharedProject`, `IsClientSideProject`,
+`IsSharedTestingProject`, `IsContainerProject`, `IsCLIProject`,
`IsWebSdkProject`, `IsAspireHostProject`, `IsBlazorWebAssemblyProject`, `IsRazorClassLibraryProject`,
`IsBlazorHostProject`, `IsBffProject`.
@@ -52,11 +53,16 @@ a non-test project grants access to `{AssemblyName}.{TestType}Tests` for each na
suffix outside it still builds, runs and gets a category — it just cannot see `internal` members.
The 53 names are in [references/test-types.md](references/test-types.md).
-### Shared and shared-testing projects
+### Shared, shared-testing and ServerShared projects
-Recognised by **exact name**, never by substring. The two lists are in
+Recognised by **exact name**, never by substring. The three lists are in
[references/detection.md](references/detection.md). Derived: `IsSharedProject`,
-`IsSharedTestingProject`.
+`IsSharedTestingProject`, `IsServerSharedProject`.
+
+`ServerShared` is separate from `Shared` for a reason: a `Shared` project is referenced by almost
+everything, including code that ends up in a browser, so server-only helpers get their own name and
+are referenced automatically only by a project that cannot be downloaded. `IsClientSideProject` is the
+classification that decides it.
### SDK and content based
diff --git a/skills/purview-buildsdk-project-detection/references/detection.md b/skills/purview-buildsdk-project-detection/references/detection.md
index 85fde09..51243c9 100644
--- a/skills/purview-buildsdk-project-detection/references/detection.md
+++ b/skills/purview-buildsdk-project-detection/references/detection.md
@@ -5,6 +5,20 @@
`Shared`, `SharedFramework`, `SharedInfrastructure`, `SharedInfra`, `SharedUtilities`,
`SharedUtils`, `SharedLibrary`, `SharedLib`, `SharedHelpers`
+## ServerShared project names (exact)
+
+`ServerShared`, `ServerSharedFramework`, `ServerSharedInfrastructure`, `ServerSharedInfra`,
+`ServerSharedUtilities`, `ServerSharedUtils`, `ServerSharedLibrary`, `ServerSharedLib`,
+`ServerSharedHelpers`
+
+A `ServerShared` project is referenced automatically by every non-client-side project, and never by a
+WebAssembly client, a `PurviewBrowserSafe` library, a Razor class library, a project named with a
+client-side segment (`*.Client`, `*.ClientShared`, `*.Browser`, `*.Wasm`, `*.WebAssembly`,
+`*.Components`, or exactly `Components`/`ClientShared`), or a `Shared`/`SharedTesting` project.
+`IsClientSideProject` overrides the classification. `PRSHR0001` reports a client-side reference and
+`PRSHR0002` a `Shared` reference; `PurviewBrowserSafe="true"` on the reference records an accepted
+decision.
+
## Shared testing project names (exact)
`SharedTestingFramework`, `SharedTestingInfrastructure`, `SharedTestingInfra`,
@@ -64,7 +78,8 @@ dual-role component that wants normal library symbol packaging.
| Container | `InvariantGlobalization`, `PublishAot`, Linux Docker defaults, container tooling packages |
| Web SDK | `Microsoft.AspNetCore.OpenApi.Generated` added to `InterceptorsNamespaces`, unless marked as a separate web-project mode |
| Aspire host | `OutputType=Exe` |
-| Blazor WebAssembly client | not packable, no container defaults, no automatic `Shared` reference, the browser-safe reference check - see [blazor.md](blazor.md) |
+| Blazor WebAssembly client | not packable, no container defaults, no automatic `Shared`/`ServerShared` reference, the browser-safe reference check - see [blazor.md](blazor.md) |
+| ServerShared project | referenced automatically by every non-client-side project; never by a WebAssembly client, browser-safe library, Razor class library, client-side name, or `Shared`/`SharedTesting` project |
| Blazor host | an automatic reference to its `.Client` - see [blazor.md](blazor.md) |
| BFF | no packages, no OpenAPI interceptor unless it exposes an API, `PDS0008`/`PDS0009` - see [blazor.md](blazor.md) |
| `PurviewBrowserSafe` library | `IsAotCompatible`, `SupportedPlatform browser`, trim/AOT/`CA1416` as errors - see [blazor.md](blazor.md) |
diff --git a/src/src/Analyzers/EnglishNaming/EnglishNamingAnalyzer.cs b/src/src/Analyzers/EnglishNaming/EnglishNamingAnalyzer.cs
index 71dbe25..587005f 100644
--- a/src/src/Analyzers/EnglishNaming/EnglishNamingAnalyzer.cs
+++ b/src/src/Analyzers/EnglishNaming/EnglishNamingAnalyzer.cs
@@ -69,9 +69,8 @@ ConcurrentDictionary configCache
return;
}
- var config = configCache.GetOrAdd(
- declaringTree,
- tree => EnglishNamingConfig.FromOptions(context.Options.AnalyzerConfigOptionsProvider.GetOptions(tree))
+ var config = configCache.GetOrAdd(declaringTree, tree =>
+ EnglishNamingConfig.FromOptions(context.Options.AnalyzerConfigOptionsProvider.GetOptions(tree))
);
var corrected = EnglishNamingHelper.CorrectIdentifier(symbol.Name, config);
diff --git a/src/src/BuildSdk/Sdk/Props/Defaults.props b/src/src/BuildSdk/Sdk/Props/Defaults.props
index 737b989..6733b4e 100644
--- a/src/src/BuildSdk/Sdk/Props/Defaults.props
+++ b/src/src/BuildSdk/Sdk/Props/Defaults.props
@@ -2,6 +2,10 @@
<_SharedProjectNames Condition="'$(_SharedProjectNames)' == ''"
>;Shared;SharedFramework;SharedInfrastructure;SharedInfra;SharedUtilities;SharedUtils;SharedLibrary;SharedLib;SharedHelpers;
+
+ <_ServerSharedProjectNames Condition="'$(_ServerSharedProjectNames)' == ''"
+ >;ServerShared;ServerSharedFramework;ServerSharedInfrastructure;ServerSharedInfra;ServerSharedUtilities;ServerSharedUtils;ServerSharedLibrary;ServerSharedLib;ServerSharedHelpers;
<_SharedTestingProjectNames Condition="'$(_SharedTestingProjectNames)' == ''"
>;SharedTestingFramework;SharedTestingInfrastructure;SharedTestingInfra;SharedTestingUtilities;SharedTestingUtils;SharedTestingLibrary;SharedTestingLib;SharedTestingHelpers;
@@ -23,6 +27,11 @@
>true
true
+
+ true
true
false
@@ -138,14 +147,32 @@
false
false
false
+ false
false
false
false
false
false
- preview
+
+ latest
true
+
+ 2
enable
@@ -160,6 +187,19 @@
TUnitMocks
Bogus
+
+ true
+
+ false
+ true
+
+ true
@@ -221,14 +261,21 @@
+
+
+ # Purview.BuildSdk - managed file
Microsoft.SourceLink.GitHub
snupkg
-
diff --git a/src/src/BuildSdk/Sdk/Sdk.props b/src/src/BuildSdk/Sdk/Sdk.props
index f150f83..e8a2b73 100644
--- a/src/src/BuildSdk/Sdk/Sdk.props
+++ b/src/src/BuildSdk/Sdk/Sdk.props
@@ -15,11 +15,13 @@
===================================================================== -->
<_SharedProjectNames>;Shared;SharedFramework;SharedInfrastructure;SharedInfra;SharedUtilities;SharedUtils;SharedLibrary;SharedLib;SharedHelpers;
+ <_ServerSharedProjectNames>;ServerShared;ServerSharedFramework;ServerSharedInfrastructure;ServerSharedInfra;ServerSharedUtilities;ServerSharedUtils;ServerSharedLibrary;ServerSharedLib;ServerSharedHelpers;
<_SharedTestingProjectNames>;SharedTestingFramework;SharedTestingInfrastructure;SharedTestingInfra;SharedTestingUtilities;SharedTestingUtils;SharedTestingLibrary;SharedTestingLib;SharedTestingHelpers;
+
@@ -41,6 +43,9 @@
repository really wants it collapsed.
===================================================================== -->
+
+
@@ -183,7 +188,7 @@
2. Only *unconditional* declarations are matched. A `Condition` attribute cannot be evaluated
at this point, and guessing either way is worse than not matching: treating a conditioned
`` as a declaration makes this SDK skip its
- `netstandard2.0` Roslyn default, the generic `net10.0` default fill in, and - when the
+ `netstandard2.0` Roslyn default, the generic default fill in, and - when the
condition does not fire - an analyzer ship on a TFM the IDE cannot load. A conditioned
declaration is reported by `PurviewValidateUnconditionalProjectDeclarations` in Sdk.targets
instead of being silently misread.
@@ -309,6 +314,9 @@
true
+ true
$(NamespacePrefix)
$(PurviewNamespacePrefix)
@@ -535,7 +543,7 @@
When true, PRBLZ0001 is reported as an error instead of a warning. Defaults to false.
- When true, a Blazor WebAssembly client keeps the generic defaults (packable when asked, container detection, the automatic Shared reference). Set before the SDK import.
+ When true, a Blazor WebAssembly client keeps the generic defaults (packable when asked, container detection, the automatic Shared reference). Set before the SDK import. ServerShared is never added to a client-side project, whatever this says.
When false, a host named Name does not automatically reference its Name.Client WebAssembly client. Defaults to true.
@@ -543,6 +551,24 @@
When true, a BFF keeps the OpenAPI interceptor namespace. Defaults to true only when the BFF references Microsoft.AspNetCore.OpenApi.
+
+ When false, a server-side project does not automatically reference a sibling ServerShared project. Client-side projects never get one. Defaults to true.
+
+
+ When true, a client-side or Shared project does not report a ServerShared project reference (PRSHR0001/PRSHR0002).
+
+
+ When true, PRSHR0001/PRSHR0002 are errors instead of warnings. Defaults to false.
+
+
+ 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 (default), boots a Directory.Build.rsp that enables MSBuild's multithreaded build (-mt).
+
Path to the SDK-provided .editorconfig used for build-time style enforcement.
@@ -558,6 +584,12 @@
When true, bootstraps global.json to RepositoryGlobalJsonFilePath if that file does not exist.
+
+ Destination path for bootstrapping a repository-level Directory.Build.rsp when missing.
+
+
+ When true, bootstraps a managed Directory.Build.rsp that enables MSBuild's multithreaded build (-mt).
+
When true, disables SDK auto-copy bootstrapping for repository files such as .editorconfig and global.json.
@@ -582,6 +614,12 @@
True when the project is a shared project.
+
+ True for ServerShared helper projects (ServerSharedFramework, etc.), which are referenced automatically only by projects that cannot run in a browser.
+
+
+ True when the project's assemblies are downloaded to a browser: a WebAssembly client, a PurviewBrowserSafe library, a Razor class library, or a project named with a client-side suffix (*.Client, *.Browser, *.Wasm, *.WebAssembly, *.Components). Set explicitly to override.
+
net11.0
-
+
net10.0
-
+ >net10.0;net11.0
+
net10.0;net11.0
+ this file - is what decides a project's targets. It resolves to every release still in
+ support, so one SDK bump moves every consumer that never chose a set. -->
Current
<_PurviewResolvedTargetFrameworkSet Condition="'$(PurviewTargetFrameworkSet)' == 'Latest'"
@@ -778,8 +817,11 @@
quietly built one TFM and every multi-targeting consumer failed to reference it with
"Project 'X' targets 'net10.0'. It cannot be referenced by a project that targets
'.NETCoreApp,Version=v9.0'". -->
+ $(PurviewTargetFrameworksCurrent)
$(PurviewTargetFrameworksCurrent)
true
<_PurviewSharedProjectRemovePattern>$([System.Text.RegularExpressions.Regex]::Replace('$(_SharedProjectNames)', '^;|;$', '').Replace(';', '|'))
+ <_PurviewServerSharedProjectRemovePattern>$([System.Text.RegularExpressions.Regex]::Replace('$(_ServerSharedProjectNames)', '^;|;$', '').Replace(';', '|'))
<_PurviewSharedTestingProjectRemovePattern>$([System.Text.RegularExpressions.Regex]::Replace('$(_SharedTestingProjectNames)', '^;|;$', '').Replace(';', '|'))
- <_PurviewNamespaceRemovePattern>$(_PurviewSharedProjectRemovePattern)|$(_PurviewSharedTestingProjectRemovePattern)|Core|EF|Shared|ClientShared|ServiceDefaults
+ <_PurviewNamespaceRemovePattern>$(_PurviewServerSharedProjectRemovePattern)|$(_PurviewSharedProjectRemovePattern)|$(_PurviewSharedTestingProjectRemovePattern)|Core|EF|Shared|ClientShared|ServiceDefaults
$(PurviewLogicalProjectName)
diff --git a/src/src/BuildSdk/Sdk/Sdk.targets b/src/src/BuildSdk/Sdk/Sdk.targets
index e7adf63..b60a2f7 100644
--- a/src/src/BuildSdk/Sdk/Sdk.targets
+++ b/src/src/BuildSdk/Sdk/Sdk.targets
@@ -9,6 +9,8 @@
+
+
+
+
+ <_DirectoryBuildPropsDirectory>$([System.IO.Path]::GetDirectoryName('$(DirectoryBuildPropsPath)'))
+
+ <_RepositoryProbe0>$(_DirectoryBuildPropsDirectory)
+ <_RepositoryProbe1>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe0)\..'))
+ <_RepositoryProbe2>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe1)\..'))
+ <_RepositoryProbe3>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe2)\..'))
+ <_RepositoryProbe4>$([System.IO.Path]::GetFullPath('$(_RepositoryProbe3)\..'))
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe0)/.git/HEAD') OR Exists('$(_RepositoryProbe0)/.git'))"
+ >$(_RepositoryProbe0)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe1)/.git/HEAD') OR Exists('$(_RepositoryProbe1)/.git'))"
+ >$(_RepositoryProbe1)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe2)/.git/HEAD') OR Exists('$(_RepositoryProbe2)/.git'))"
+ >$(_RepositoryProbe2)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe3)/.git/HEAD') OR Exists('$(_RepositoryProbe3)/.git'))"
+ >$(_RepositoryProbe3)
+ <_DetectedRepositoryRoot
+ Condition="'$(_DetectedRepositoryRoot)' == '' AND (Exists('$(_RepositoryProbe4)/.git/HEAD') OR Exists('$(_RepositoryProbe4)/.git'))"
+ >$(_RepositoryProbe4)
+ <_EffectiveRepositoryDirectoryBuildRspFilePath>$(RepositoryDirectoryBuildRspFilePath)
+ <_EffectiveRepositoryDirectoryBuildRspFilePath
+ Condition="'$(_EffectiveRepositoryDirectoryBuildRspFilePath)' == '' AND '$(_DetectedRepositoryRoot)' != ''"
+ >$([System.IO.Path]::Combine('$(_DetectedRepositoryRoot)', 'Directory.Build.rsp'))
+ <_EffectiveRepositoryDirectoryBuildRspFilePath
+ Condition="'$(_EffectiveRepositoryDirectoryBuildRspFilePath)' == '' AND '$(_DirectoryBuildPropsDirectory)' != ''"
+ >$([System.IO.Path]::Combine('$(_DirectoryBuildPropsDirectory)', 'Directory.Build.rsp'))
+ <_PurviewRspEnabled Condition="'$(PurviewMultiThreadedBuild)' != 'false'">true
+ <_PurviewRspEnabled Condition="'$(_PurviewRspEnabled)' == ''">false
+
+
+ $(_EffectiveRepositoryDirectoryBuildRspFilePath)
+
+
+ <_RepositoryDirectoryBuildRspLines Include="$(PurviewRepositoryRspVersionMarker)" />
+ <_RepositoryDirectoryBuildRspLines Include="# Managed by Purview.BuildSdk. Delete the marker line to take ownership of this file." />
+ <_RepositoryDirectoryBuildRspLines Condition="'$(_PurviewRspEnabled)' == 'true'" Include="-mt" />
+
+ <_RepositoryDirectoryBuildRspLines Condition="'$(_PurviewRspEnabled)' == 'true'" Include="-nr:false" />
+ <_RepositoryDirectoryBuildRspLines Condition="'$(_PurviewRspEnabled)' != 'true'" Include="-mt:false" />
+
+
+ <_PurviewRspDestinationExists Condition="'$(_EffectiveRepositoryDirectoryBuildRspFilePath)' != ''"
+ >$([System.IO.File]::Exists('$(_EffectiveRepositoryDirectoryBuildRspFilePath)'))
+ <_PurviewRspDestinationText Condition="'$(_PurviewRspDestinationExists)' == 'true'"
+ >$([System.IO.File]::ReadAllText('$(_EffectiveRepositoryDirectoryBuildRspFilePath)'))
+ <_PurviewRspDestinationFirstLine Condition="'$(_PurviewRspDestinationExists)' == 'true'"
+ >$([System.Text.RegularExpressions.Regex]::Match($(_PurviewRspDestinationText), `^[^\r\n]*`).Value)
+ <_PurviewRspIsManaged
+ Condition="'$(_PurviewRspDestinationExists)' == 'true' AND $(_PurviewRspDestinationFirstLine.StartsWith('$(PurviewRepositoryRspVersionMarker)'))"
+ >true
+ <_PurviewRspHasEnabledSwitch
+ Condition="'$(_PurviewRspDestinationExists)' == 'true' AND $([System.Text.RegularExpressions.Regex]::IsMatch($(_PurviewRspDestinationText), `(?m)^\s*-mt\s*$`))"
+ >true
+ <_PurviewRspHasNoNodeReuse
+ Condition="'$(_PurviewRspDestinationExists)' == 'true' AND $([System.Text.RegularExpressions.Regex]::IsMatch($(_PurviewRspDestinationText), `(?m)^\s*-nr\s*:\s*false\s*$`))"
+ >true
+ <_PurviewRspHasDisabledSwitch
+ Condition="'$(_PurviewRspDestinationExists)' == 'true' AND $([System.Text.RegularExpressions.Regex]::IsMatch($(_PurviewRspDestinationText), `(?m)^\s*-mt\s*:\s*false\s*$`))"
+ >true
+ <_PurviewRspContentMatches
+ Condition="'$(_PurviewRspEnabled)' == 'true' AND '$(_PurviewRspHasEnabledSwitch)' == 'true' AND '$(_PurviewRspHasNoNodeReuse)' == 'true'"
+ >true
+ <_PurviewRspContentMatches
+ Condition="'$(_PurviewRspEnabled)' != 'true' AND '$(_PurviewRspHasDisabledSwitch)' == 'true'"
+ >true
+
+ <_PurviewRspNeedsSync
+ Condition="'$(_PurviewRspDestinationExists)' != 'true' AND '$(_PurviewRspEnabled)' == 'true'"
+ >true
+ <_PurviewRspNeedsSync
+ Condition="'$(_PurviewRspDestinationExists)' == 'true' AND '$(_PurviewRspIsManaged)' == 'true' AND '$(_PurviewRspContentMatches)' != 'true'"
+ >true
+ <_PurviewRspNeedsSync
+ Condition="'$(PurviewRepoBootstrapMode)' == 'Always' OR '$(PurviewRepoBootstrapMode)' == 'WarnOnDrift'"
+ >true
+ <_PurviewRspOverwrite
+ Condition="'$(_PurviewRepoBootstrapOverwriteExisting)' == 'true' OR '$(_PurviewRspIsManaged)' == 'true'"
+ >true
+ <_PurviewRspOverwrite Condition="'$(_PurviewRspOverwrite)' == ''">false
+
+
+
+
$(MSBuildProjectName)
$(IsBlazorWebAssemblyProject)
$(PurviewBrowserSafe)
+
+ $(IsServerSharedProject)
+ $(IsClientSideProject)
diff --git a/src/src/BuildSdk/Sdk/Targets/Modern.targets b/src/src/BuildSdk/Sdk/Targets/Modern.targets
new file mode 100644
index 0000000..3a155db
--- /dev/null
+++ b/src/src/BuildSdk/Sdk/Targets/Modern.targets
@@ -0,0 +1,95 @@
+
+
+
+
+ <_PurviewTargetFrameworkMajor
+ Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(TargetFramework)', '^net\d+\.'))"
+ >$([System.Text.RegularExpressions.Regex]::Match('$(TargetFramework)', '^net(?<major>\d+)\.').Groups['major'].Value)
+ <_PurviewTargetsNet11OrLater
+ Condition="'$(_PurviewTargetFrameworkMajor)' != '' AND $([System.Int32]::Parse('$(_PurviewTargetFrameworkMajor)')) >= 11"
+ >true
+
+
+
+ true
+ 2
+ <_PurviewMemorySafetyRulesNormalized>$([System.String]::Copy('$(MemorySafetyRules)').Trim().ToLowerInvariant())
+
+ <_PurviewMemorySafetyRulesNormalized
+ Condition="'$(_PurviewMemorySafetyRulesNormalized)' == 'off' OR '$(_PurviewMemorySafetyRulesNormalized)' == 'none' OR '$(_PurviewMemorySafetyRulesNormalized)' == 'false'"
+ >0
+ <_PurviewRuntimeAsyncEnabled Condition="'$(PurviewRuntimeAsync)' != 'false' AND '$(UseRuntimeAsync)' != 'false'"
+ >true
+
+
+
+
+ <_PurviewFeatureSwitches>$(Features)
+ <_PurviewFeatureSwitches
+ Condition="'$(_PurviewRuntimeAsyncEnabled)' == 'true' AND !$([System.String]::Copy(';$(Features);').Contains(';runtime-async=on;'))"
+ >$(Features);runtime-async=on
+ <_PurviewFeatureSwitches
+ Condition="'$(_PurviewMemorySafetyRulesNormalized)' == '2' AND !$([System.String]::Copy(';$(_PurviewFeatureSwitches);').Contains(';updated-memory-safety-rules;'))"
+ >$(_PurviewFeatureSwitches);updated-memory-safety-rules
+ $([System.String]::Copy('$(_PurviewFeatureSwitches)').Trim(';'))
+
+
+
+
+ preview
+
+
+
+
+
+
diff --git a/src/src/BuildSdk/Sdk/Targets/ServerShared.targets b/src/src/BuildSdk/Sdk/Targets/ServerShared.targets
new file mode 100644
index 0000000..0805d45
--- /dev/null
+++ b/src/src/BuildSdk/Sdk/Targets/ServerShared.targets
@@ -0,0 +1,146 @@
+
+
+
+ <_PurviewClientSideProjectName>$([System.String]::Copy('$(MSBuildProjectName)').ToLowerInvariant())
+ true
+ true
+ true
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_PurviewServerSharedCandidate
+ Include="@(_PurviewBrowserProjectReference->'%(FullPath)')"
+ Condition="'%(_PurviewBrowserProjectReference.PurviewBrowserSafe)' != 'true'"
+ />
+
+
+
+
+
+ <_PurviewServerSharedReference Include="@(_PurviewServerSharedReferenceInfo->WithMetadataValue('IsServerSharedProject', 'true'))" />
+
+
+ <_PurviewServerSharedReferenceCode Condition="'$(IsClientSideProject)' == 'true'"
+ >PRSHR0001
+ <_PurviewServerSharedReferenceCode Condition="'$(IsClientSideProject)' != 'true'"
+ >PRSHR0002
+ <_PurviewServerSharedReferenceRole Condition="'$(IsClientSideProject)' == 'true'"
+ >a client-side project, so its assemblies are downloaded to a browser
+ <_PurviewServerSharedReferenceRole Condition="'$(IsClientSideProject)' != 'true'"
+ >a Shared project, so every project that references it inherits the reference
+ <_PurviewServerSharedReferenceText>ServerShared code belongs on the server only. Remove the reference, move the code into Shared if it is browser-safe, or set PurviewBrowserSafe="true" on the ProjectReference to record that it was judged safe. Set IsClientSideProject=false on this project if the detection is wrong, or suppress with DisablePurviewServerSharedReferenceCheck=true.
+
+
+
+
+
diff --git a/src/src/CodeFixers/ExtensionsNamespace/SplitExtensionsClassCodeRefactoringProvider.cs b/src/src/CodeFixers/ExtensionsNamespace/SplitExtensionsClassCodeRefactoringProvider.cs
index 8a16e2d..35c318c 100644
--- a/src/src/CodeFixers/ExtensionsNamespace/SplitExtensionsClassCodeRefactoringProvider.cs
+++ b/src/src/CodeFixers/ExtensionsNamespace/SplitExtensionsClassCodeRefactoringProvider.cs
@@ -541,10 +541,8 @@ CancellationToken cancellationToken
continue;
}
- var renamedRoot = root.ReplaceNodes(
- root.DescendantNodes().OfType(),
- (node, _) =>
- node.Identifier.Text == oldName ? node.WithIdentifier(SyntaxFactory.Identifier(newName)) : node
+ var renamedRoot = root.ReplaceNodes(root.DescendantNodes().OfType(), (node, _) =>
+ node.Identifier.Text == oldName ? node.WithIdentifier(SyntaxFactory.Identifier(newName)) : node
);
if (!renamedRoot.IsEquivalentTo(root, topLevel: false))
{
diff --git a/src/src/SkillsTesting/SkillCatalogue.cs b/src/src/SkillsTesting/SkillCatalogue.cs
index 80d9c83..a1093d4 100644
--- a/src/src/SkillsTesting/SkillCatalogue.cs
+++ b/src/src/SkillsTesting/SkillCatalogue.cs
@@ -46,8 +46,7 @@ public sealed class SkillCatalogue
/// No repository root could be resolved.
public static SkillCatalogue ForRepository(string? repositoryRoot = null, string folderName = DefaultFolderName)
{
- var root =
- repositoryRoot
+ var root = repositoryRoot
?? SkillRepositoryRoot.Find()
?? throw new InvalidOperationException(
$"Could not resolve a repository root above '{AppContext.BaseDirectory}'. Expected one of "
diff --git a/src/src/SkillsTesting/SkillManifestReader.cs b/src/src/SkillsTesting/SkillManifestReader.cs
index d189300..b8026eb 100644
--- a/src/src/SkillsTesting/SkillManifestReader.cs
+++ b/src/src/SkillsTesting/SkillManifestReader.cs
@@ -91,8 +91,8 @@ public static SkillManifest Parse(string folderName, string skillDirectory, stri
List errors = [];
List unknown = [];
- Dictionary fields = new(StringComparer.Ordinal);
- Dictionary metadata = new(StringComparer.Ordinal);
+ Dictionary fields = [with(StringComparer.Ordinal)];
+ Dictionary metadata = [with(StringComparer.Ordinal)];
for (var index = 1; index < closing; index++)
{
diff --git a/src/src/SkillsTesting/SkillRepositoryRoot.cs b/src/src/SkillsTesting/SkillRepositoryRoot.cs
index 1600c57..f4b6fd9 100644
--- a/src/src/SkillsTesting/SkillRepositoryRoot.cs
+++ b/src/src/SkillsTesting/SkillRepositoryRoot.cs
@@ -119,7 +119,7 @@ public static (string? Root, string? Marker) FindWithMarker(string startDirector
/// The candidate directories, nearest first.
static List Ancestors(string startDirectory)
{
- HashSet excluded = new(StringComparer.OrdinalIgnoreCase);
+ HashSet excluded = [with(StringComparer.OrdinalIgnoreCase)];
try
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
diff --git a/src/tests/Analyzers.IntegrationTests/Unions/UnionEqualitySuppressorTests.cs b/src/tests/Analyzers.IntegrationTests/Unions/UnionEqualitySuppressorTests.cs
index 1c05683..fda6322 100644
--- a/src/tests/Analyzers.IntegrationTests/Unions/UnionEqualitySuppressorTests.cs
+++ b/src/tests/Analyzers.IntegrationTests/Unions/UnionEqualitySuppressorTests.cs
@@ -18,9 +18,10 @@ namespace Purview.BuildSdk.Analyzers.Unions;
/// by it at all.
///
///
-/// Every source here declares UnionAttribute and IUnion itself. Unions are a preview language
-/// feature whose required runtime types are not in the net10.0 framework this test host references, and
-/// without IUnion the union declaration fails to bind with CS0518 and no symbol is produced.
+/// Every source here declares UnionAttribute and IUnion itself. The runtime types ship with
+/// net11.0 but not net10.0, and this suite runs on both; without IUnion the union
+/// declaration fails to bind with CS0518 and no symbol is produced. The compiler embeds its own copies
+/// in exactly the same way.
///
///
[Category("Integration")]
diff --git a/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs b/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs
index 7f306b6..cea99ce 100644
--- a/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/CompilerVisiblePropertyTests.cs
@@ -62,6 +62,17 @@ public async Task CompilerVisibleProperties_Include_AllSdkProperties(Cancellatio
"DisablePurviewBlazorWebAssemblyDefaults",
"PurviewAutoBlazorClientReference",
"PurviewBffExposesApi",
+ "IsSharedProject",
+ "IsServerSharedProject",
+ "IsClientSideProject",
+ "PurviewAutoServerSharedReference",
+ "DisablePurviewServerSharedReferenceCheck",
+ "PurviewServerSharedReferenceCheckAsError",
+ "MemorySafetyRules",
+ "PurviewRuntimeAsync",
+ "PurviewMultiThreadedBuild",
+ "RepositoryDirectoryBuildRspFilePath",
+ "BootstrapMultiThreadedBuildRspToRepoRoot",
"EditorConfigFilePath",
"CurrentYear",
"AutoGeneratedAssemblyInfoFile",
diff --git a/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs b/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs
index 29b258f..8ccfb2f 100644
--- a/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/CoreDefaultsTests.cs
@@ -40,8 +40,8 @@ await Assert
.Because("ImplicitUsings should default to enable");
await Assert
.That(eval.Properties["LangVersion"])
- .IsEqualTo("preview")
- .Because("LangVersion should default to preview");
+ .IsEqualTo("latest")
+ .Because("LangVersion should default to the latest shipped C# version (C# 15)");
await Assert
.That(eval.Properties["Deterministic"])
.IsEqualTo("true")
@@ -108,7 +108,6 @@ CancellationToken cancellationToken
extraProps: "false",
cancellationToken: cancellationToken
);
- // The SDK sets LangVersion to "preview" unless explicitly overridden.
await Assert
.That(await h.GetPropertyAsync("WarnOnPackingNonPackableProject", cancellationToken))
.IsEqualTo("false");
diff --git a/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs b/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs
new file mode 100644
index 0000000..1811df3
--- /dev/null
+++ b/src/tests/BuildSdk.IntegrationTests/Dotnet11FeaturesTests.cs
@@ -0,0 +1,183 @@
+using Purview.BuildSdk.Harness;
+
+namespace Purview.BuildSdk;
+
+///
+/// Pins the .NET 11 compiler feature switches the SDK applies to a net11.0 (or later) target:
+/// Runtime Async and the C# 15 updated memory-safety rules.
+///
+///
+/// The default target set is net10.0;net11.0, so the switches have to be per-TFM: applying
+/// them in Sdk.props would enable them for the net10.0 leg as well, where
+/// runtime-async still needs EnablePreviewFeatures. AllowUnsafeBlocks is deliberately
+/// part of the contract: the updated model is most useful with unsafe code still disallowed.
+///
+sealed class Dotnet11FeaturesTests
+{
+ [Test]
+ public async Task Net11Target_EnablesRuntimeAsyncAndUpdatedMemorySafetyRules(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net11.0",
+ cancellationToken: cancellationToken
+ );
+
+ var eval = await h.EvaluateAsync(
+ ["Features", "MemorySafetyRules", "AllowUnsafeBlocks", "LangVersion"],
+ cancellationToken: cancellationToken
+ );
+
+ await Assert
+ .That(eval.Properties["Features"])
+ .Contains("runtime-async=on")
+ .Because("Runtime Async V2 is the .NET 11 default and no longer needs EnablePreviewFeatures");
+ await Assert
+ .That(eval.Properties["Features"])
+ .Contains("updated-memory-safety-rules")
+ .Because("MemorySafetyRules defaults to tier 2");
+ await Assert.That(eval.Properties["MemorySafetyRules"]).IsEqualTo("2");
+ await Assert
+ .That(eval.Properties["AllowUnsafeBlocks"])
+ .IsEqualTo("false")
+ .Because("the updated rules must not quietly turn unsafe code on");
+ await Assert
+ .That(eval.Properties["LangVersion"])
+ .IsEqualTo("latest")
+ .Because("tier 2 is a feature switch and does not require the preview language version");
+ }
+
+ [Test]
+ public async Task Net10Target_DoesNotReceiveTheNet11Switches(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net10.0",
+ cancellationToken: cancellationToken
+ );
+
+ var features = await h.GetPropertyAsync("Features", cancellationToken);
+
+ await Assert.That(features).DoesNotContain("runtime-async=on");
+ await Assert.That(features).DoesNotContain("updated-memory-safety-rules");
+ }
+
+ [Test]
+ public async Task Net11Target_KeepsFeaturesTheProjectAlreadySet(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net11.0",
+ extraProps: "$(Features);project-feature=on",
+ cancellationToken: cancellationToken
+ );
+
+ 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)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net11.0",
+ extraProps: "false",
+ cancellationToken: cancellationToken
+ );
+
+ var features = await h.GetPropertyAsync("Features", cancellationToken);
+
+ await Assert.That(features).DoesNotContain("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.
+ ///
+ [Test]
+ public async Task RuntimeAsync_HonoursTheDocumentedUseRuntimeAsyncOptOut(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net11.0",
+ extraProps: "false",
+ cancellationToken: cancellationToken
+ );
+
+ await Assert.That(await h.GetPropertyAsync("Features", cancellationToken)).DoesNotContain("runtime-async=on");
+ }
+
+ [Test]
+ public async Task MemorySafetyTierZero_DisablesTheUpdatedRules(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net11.0",
+ extraProps: "0",
+ cancellationToken: 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");
+ }
+
+ ///
+ /// Tier 1 is the preview syntax, which is a language-version change rather than a feature switch.
+ ///
+ [Test]
+ public async Task MemorySafetyTierOne_EnablesPreviewSyntax(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net11.0",
+ extraProps: "1",
+ cancellationToken: cancellationToken
+ );
+
+ var eval = await h.EvaluateAsync(["Features", "LangVersion"], cancellationToken: cancellationToken);
+
+ await Assert.That(eval.Properties["LangVersion"]).IsEqualTo("preview");
+ await Assert.That(eval.Properties["Features"]).DoesNotContain("updated-memory-safety-rules");
+ }
+
+ [Test]
+ public async Task MemorySafetyTierOne_KeepsAnExplicitLanguageVersion(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net11.0",
+ extraProps: "115.0",
+ cancellationToken: cancellationToken
+ );
+
+ await Assert.That(await h.GetPropertyAsync("LangVersion", cancellationToken)).IsEqualTo("15.0");
+ }
+
+ [Test]
+ public async Task UnrecognisedMemorySafetyTier_FailsTheBuild(CancellationToken cancellationToken)
+ {
+ using var h = await ProjectHarness.CreateAsync(
+ "MyLibrary",
+ targetFramework: "net11.0",
+ extraProps: "3",
+ cancellationToken: cancellationToken
+ );
+
+ // The validation target, rather than a full build: the failure under test is evaluation-time and
+ // a full build would also need the harness's telemetry package references restored.
+ var (code, output, errors) = await h.RunMSBuildAsync(
+ "-t:ValidatePurviewModernFeatureSettings",
+ cancellationToken
+ );
+
+ await Assert.That(code).IsNotZero();
+ await Assert.That(output + errors).Contains("PRCFG0001");
+ }
+}
diff --git a/src/tests/BuildSdk.IntegrationTests/Harness/ProjectHarness.cs b/src/tests/BuildSdk.IntegrationTests/Harness/ProjectHarness.cs
index ccf4a3f..a6d6a37 100644
--- a/src/tests/BuildSdk.IntegrationTests/Harness/ProjectHarness.cs
+++ b/src/tests/BuildSdk.IntegrationTests/Harness/ProjectHarness.cs
@@ -186,6 +186,10 @@ await File.WriteAllTextAsync(
{namespacePrefix}
+
+ false
{preImportBlock}
@@ -240,8 +244,7 @@ async Task CreateOrUpdateSolution(string solutionPath, string projectName, Cance
if (File.Exists(solutionPath))
{
var document = XDocument.Load(solutionPath);
- var solution =
- document.Root
+ var solution = document.Root
?? throw new InvalidOperationException($"The solution file '{solutionPath}' has no root element.");
var alreadyExists = solution
diff --git a/src/tests/BuildSdk.IntegrationTests/IncrementalBuildTests.cs b/src/tests/BuildSdk.IntegrationTests/IncrementalBuildTests.cs
index 2e87512..9a39b3b 100644
--- a/src/tests/BuildSdk.IntegrationTests/IncrementalBuildTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/IncrementalBuildTests.cs
@@ -51,8 +51,11 @@ static async Task AssertGeneratedFileIsStable(string fileName, CancellationToken
cancellationToken: cancellationToken
);
- var first = await harness.BuildAsync(restore: true, cancellationToken: cancellationToken);
- await Assert.That(first.Success).IsTrue().Because(TestHelpers.GenerateError(first.Output, first.Errors));
+ var (successFirst, outputFirst, errorsFirst) = await harness.BuildAsync(
+ restore: true,
+ cancellationToken: cancellationToken
+ );
+ await Assert.That(successFirst).IsTrue().Because(TestHelpers.GenerateError(outputFirst, errorsFirst));
var generatedPath = FindGeneratedFile(harness, fileName);
var generatedContent = await File.ReadAllTextAsync(generatedPath, cancellationToken);
@@ -63,8 +66,10 @@ static async Task AssertGeneratedFileIsStable(string fileName, CancellationToken
// against, so the delay cannot be removed.
await Task.Delay(TimeSpan.FromMilliseconds(50), cancellationToken);
- var second = await harness.BuildAsync(cancellationToken: cancellationToken);
- await Assert.That(second.Success).IsTrue().Because(TestHelpers.GenerateError(second.Output, second.Errors));
+ var (successSecond, outputSecond, errorsSecond) = await harness.BuildAsync(
+ cancellationToken: cancellationToken
+ );
+ await Assert.That(successSecond).IsTrue().Because(TestHelpers.GenerateError(outputSecond, errorsSecond));
await Assert
.That(File.GetLastWriteTimeUtc(generatedPath))
diff --git a/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs b/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs
index 626886f..eb9c653 100644
--- a/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/InternalsVisibleToTests.cs
@@ -105,7 +105,7 @@ public async Task InternalsVisibleTo_NeverGrantsToAnEmptyAssemblyName(Cancellati
using var h = await CreateBuildableLibraryAsync(cancellationToken);
var malformed = (await ReadInternalsVisibleToGrantsAsync(h, cancellationToken))
- .Where(static grant => grant.StartsWith('.'))
+ .Where(static grant => grant.Length > 0 && grant[0] == '.')
.ToArray();
// TargetProjectName has no default, so an unguarded '$(TargetProjectName).%(TestType)Tests'
@@ -172,9 +172,11 @@ CancellationToken cancellationToken
continue;
start += marker.Length;
- var end = line.IndexOf('"', start);
- if (end > start || end == start)
- grants.Add(line[start..end]);
+ // Search the span rather than IndexOf(char, int): CA1307 wants a StringComparison overload
+ // on net11, which net10 does not have, so the char overload is the one clean on both.
+ var length = line.AsSpan(start).IndexOf('"');
+ if (length >= 0)
+ grants.Add(line[start..(start + length)]);
}
return grants;
diff --git a/src/tests/BuildSdk.IntegrationTests/PackagedVersionFallbackTests.cs b/src/tests/BuildSdk.IntegrationTests/PackagedVersionFallbackTests.cs
index ffb09e3..5b9e4ba 100644
--- a/src/tests/BuildSdk.IntegrationTests/PackagedVersionFallbackTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/PackagedVersionFallbackTests.cs
@@ -354,7 +354,7 @@ public async Task AdoptionPin_MatchesPackageJson(string relativePath, Cancellati
.Select(match => (Line: index + 1, Version: match.Groups["version"].Value))
)
// The property reference in the auto-detection documentation is not a pin.
- .Where(pin => !pin.Version.StartsWith('$'))
+ .Where(pin => pin.Version.Length == 0 || pin.Version[0] != '$')
.ToArray();
await Assert.That(pins).IsNotEmpty().Because($"{relativePath} is expected to pin Purview.BuildSdk.");
diff --git a/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs b/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs
index d408ed2..4030050 100644
--- a/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs
@@ -68,10 +68,14 @@ public async Task EditorConfigBootstrap_ExistingFile_IsNeverOverwritten(Cancella
[Test]
public async Task DesignTimeBuild_DoesNotBootstrapTheRepositoryFiles(CancellationToken cancellationToken)
{
- using var h = await CreateHarnessAsync(extraProps: null, cancellationToken: cancellationToken);
+ using var h = await CreateHarnessAsync(
+ extraProps: "true",
+ cancellationToken: cancellationToken
+ );
var editorConfigPath = Path.Combine(h.SolutionDirectory, ".editorconfig");
var globalJsonPath = Path.Combine(h.SolutionDirectory, "global.json");
+ var rspPath = Path.Combine(h.SolutionDirectory, "Directory.Build.rsp");
// A real build first: it bootstraps both files and, just as importantly, restores so the
// design-time pass below has an assets file to read.
@@ -83,10 +87,12 @@ public async Task DesignTimeBuild_DoesNotBootstrapTheRepositoryFiles(Cancellatio
await Assert.That(firstSuccess).IsTrue().Because(TestHelpers.GenerateError(firstOutput, firstErrors));
await Assert.That(File.Exists(editorConfigPath)).IsTrue().Because("a real build bootstraps it");
await Assert.That(File.Exists(globalJsonPath)).IsTrue().Because("a real build bootstraps it");
+ await Assert.That(File.Exists(rspPath)).IsTrue().Because("a real build bootstraps it");
// Remove them, so the design-time pass has something it could do and visibly does not.
File.Delete(editorConfigPath);
File.Delete(globalJsonPath);
+ File.Delete(rspPath);
var (code, output, errors) = await h.RunMSBuildAsync(
"-t:Build -p:DesignTimeBuild=true -p:SkipCompilerExecution=true",
@@ -96,6 +102,7 @@ public async Task DesignTimeBuild_DoesNotBootstrapTheRepositoryFiles(Cancellatio
await Assert.That(code).IsZero().Because(output + errors);
await Assert.That(File.Exists(editorConfigPath)).IsFalse();
await Assert.That(File.Exists(globalJsonPath)).IsFalse();
+ await Assert.That(File.Exists(rspPath)).IsFalse();
}
///
@@ -274,7 +281,7 @@ await Assert
public async Task RepoBootstrapModeNever_SkipsAllBootstrapWrites(CancellationToken cancellationToken)
{
using var h = await CreateHarnessAsync(
- extraProps: "Never",
+ extraProps: "Nevertrue",
cancellationToken: cancellationToken
);
@@ -283,6 +290,75 @@ public async Task RepoBootstrapModeNever_SkipsAllBootstrapWrites(CancellationTok
await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors));
await Assert.That(File.Exists(Path.Combine(h.SolutionDirectory, "global.json"))).IsFalse();
await Assert.That(File.Exists(Path.Combine(h.SolutionDirectory, ".editorconfig"))).IsFalse();
+ await Assert.That(File.Exists(Path.Combine(h.SolutionDirectory, "Directory.Build.rsp"))).IsFalse();
+ }
+
+ [Test]
+ public async Task DirectoryBuildRspBootstrap_CreatesAManagedFileThatEnablesMultithreadedBuild(
+ CancellationToken cancellationToken
+ )
+ {
+ using var h = await CreateHarnessAsync(
+ extraProps: "true",
+ cancellationToken: cancellationToken
+ );
+
+ var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken);
+
+ await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors));
+
+ var rspPath = Path.Combine(h.SolutionDirectory, "Directory.Build.rsp");
+ await Assert.That(File.Exists(rspPath)).IsTrue();
+
+ var lines = await File.ReadAllLinesAsync(rspPath, cancellationToken);
+ await Assert.That(lines[0]).StartsWith("# Purview.BuildSdk - managed file");
+ await Assert.That(lines).Contains("-mt");
+ await Assert
+ .That(lines)
+ .Contains("-nr:false")
+ .Because(
+ "switching from a non-mt build must not reuse a node with a cached RoslynCodeTaskFactory task (dotnet/msbuild#15213)"
+ );
+ }
+
+ [Test]
+ public async Task DirectoryBuildRspBootstrap_LeavesAUserOwnedFileAlone(CancellationToken cancellationToken)
+ {
+ using var h = await CreateHarnessAsync(
+ extraProps: "true",
+ cancellationToken: cancellationToken
+ );
+
+ var rspPath = Path.Combine(h.SolutionDirectory, "Directory.Build.rsp");
+ const string owned = "# my own switches\n-mt:false\n";
+ await File.WriteAllTextAsync(rspPath, owned, cancellationToken);
+
+ var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken);
+
+ await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors));
+ await Assert.That(await File.ReadAllTextAsync(rspPath, cancellationToken)).IsEqualTo(owned);
+ }
+
+ [Test]
+ public async Task DirectoryBuildRspBootstrap_OptOut_RewritesAManagedFileToDisableMultithreading(
+ CancellationToken cancellationToken
+ )
+ {
+ using var h = await CreateHarnessAsync(
+ extraProps: "truefalse",
+ cancellationToken: cancellationToken
+ );
+
+ var rspPath = Path.Combine(h.SolutionDirectory, "Directory.Build.rsp");
+ await File.WriteAllTextAsync(rspPath, "# Purview.BuildSdk - managed file\n-mt\n-nr:false\n", cancellationToken);
+
+ var (success, output, errors) = await h.BuildAsync(restore: true, verbose: true, cancellationToken);
+
+ await Assert.That(success).IsTrue().Because(TestHelpers.GenerateError(output, errors));
+
+ var lines = await File.ReadAllLinesAsync(rspPath, cancellationToken);
+ await Assert.That(lines).Contains("-mt:false");
+ await Assert.That(lines).DoesNotContain("-mt");
}
[Test]
diff --git a/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs b/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs
index d1412f1..a3e7ade 100644
--- a/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/RepositorySkillsSelfTests.cs
@@ -206,6 +206,7 @@ static async Task TrackedFilesUnderAsync(string repositoryRelativePath
if (exitCode != 0)
return [];
+ // Git outputs paths with forward slashes even on Windows, so we can split on '\n' and trim whitespace without worrying about the platform.
return
[
.. stdOut
diff --git a/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs b/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs
index 12ad6eb..c659ed7 100644
--- a/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/RoslynComponentDefaultsTests.cs
@@ -132,9 +132,15 @@ public async Task CommentedOutIsRoslynComponent_DoesNotClassifyTheProject(Cancel
// Misclassifying a plain library as a Roslyn component forces netstandard2.0 and
// TreatWarningsAsErrors onto it.
- var properties = await harness.GetPropertiesAsync(cancellationToken, "IsRoslynComponent", "TargetFramework");
+ var properties = await harness.GetPropertiesAsync(
+ cancellationToken,
+ "IsRoslynComponent",
+ "TargetFramework",
+ "TargetFrameworks"
+ );
await Assert.That(properties["IsRoslynComponent"]).IsEqualTo("false");
- await Assert.That(properties["TargetFramework"]).IsEqualTo("net10.0");
+ await Assert.That(properties["TargetFramework"]).IsEmpty();
+ await Assert.That(properties["TargetFrameworks"]).IsEqualTo("net10.0;net11.0");
}
[Test]
diff --git a/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs b/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs
index 9b84ae7..65a63ac 100644
--- a/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/SdkPackageConsumptionTests.cs
@@ -261,14 +261,38 @@ await Assert
.Because("A shipped analyzer rule (PDS0003) must surface as a warning in the consumer build.");
}
+ ///
+ /// Both test assemblies target a different framework and Microsoft.Testing.Platform runs them in
+ /// parallel processes, so on the tests only serializes within one
+ /// assembly. A cross-process file lock stops the two from packing the same project into the same
+ /// obj/bin at once, which otherwise fails with CS2012.
+ ///
+ static readonly string PackLockPath = Path.Combine(Path.GetTempPath(), "purview-buildsdk-sdk-pack.lock");
+
+ static async Task AcquirePackLockAsync(CancellationToken cancellationToken)
+ {
+ while (true)
+ {
+ try
+ {
+ return new FileStream(PackLockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
+ }
+ catch (IOException)
+ {
+ // Held by the other target framework's test run; wait for it to finish packing.
+ await Task.Delay(250, cancellationToken);
+ }
+ }
+ }
+
static async Task PackSdkAsync(string feedDirectory, CancellationToken cancellationToken)
{
var sdkProjectPath = Path.GetFullPath(Path.Combine(SdkPaths.SdkDirectory, "..", "BuildSdk.csproj"));
- var sdkProjectDirectory =
- Path.GetDirectoryName(sdkProjectPath)
+ var sdkProjectDirectory = Path.GetDirectoryName(sdkProjectPath)
?? throw new InvalidOperationException("Unable to determine SDK project directory.");
var packageVersion = $"0.0.0-integration-test-{Guid.NewGuid():N}";
+ using var packLock = await AcquirePackLockAsync(cancellationToken);
var (code, stdOut, stdErr) = await ProcessRunner.RunAsync(
"dotnet",
$"pack \"{sdkProjectPath}\" -c Release -o \"{feedDirectory}\" -p:PackageVersion={packageVersion} -p:Version={packageVersion}",
diff --git a/src/tests/BuildSdk.IntegrationTests/ServerSharedTests.cs b/src/tests/BuildSdk.IntegrationTests/ServerSharedTests.cs
new file mode 100644
index 0000000..dc005b6
--- /dev/null
+++ b/src/tests/BuildSdk.IntegrationTests/ServerSharedTests.cs
@@ -0,0 +1,370 @@
+using Purview.BuildSdk.Harness;
+using Purview.BuildSdk.Infra;
+
+namespace Purview.BuildSdk;
+
+///
+/// Pins ServerShared: a Shared-style project that is referenced automatically only by projects
+/// that cannot be downloaded to a browser, plus the checks that stop it reaching one anyway.
+///
+///
+/// Detection is tested by evaluation and the reference check by invoking its target, matching the
+/// browser-safe suite: both ask the same question without restoring WebAssembly packages.
+///
+sealed class ServerSharedTests
+{
+ static Task CreateAsync(string projectName, string content, CancellationToken cancellationToken) =>
+ BlazorFixtures.CreateAsync(projectName, content, cancellationToken: cancellationToken);
+
+ static Task AddSiblingAsync(
+ ProjectHarness owner,
+ string projectName,
+ string content,
+ CancellationToken cancellationToken
+ ) => BlazorFixtures.AddSiblingAsync(owner, projectName, content, cancellationToken: cancellationToken);
+
+ static async Task> ReferencesAsync(
+ ProjectHarness project,
+ CancellationToken cancellationToken
+ ) =>
+ (await project.EvaluateAsync(itemTypes: ["ProjectReference"], cancellationToken: cancellationToken)).Items[
+ "ProjectReference"
+ ];
+
+ static async Task CreateWithServerSharedSiblingAsync(
+ string projectName,
+ string projectProperties,
+ CancellationToken cancellationToken
+ )
+ {
+ var project = await CreateAsync(projectName, BlazorFixtures.Library(projectProperties), cancellationToken);
+
+ using var serverShared = await AddSiblingAsync(
+ project,
+ "ServerShared",
+ BlazorFixtures.Library(),
+ cancellationToken
+ );
+
+ return project;
+ }
+
+ [Test]
+ public async Task ServerProject_AutomaticallyReferencesServerShared(CancellationToken cancellationToken)
+ {
+ using var project = await CreateWithServerSharedSiblingAsync("Api", string.Empty, cancellationToken);
+
+ var references = await ReferencesAsync(project, cancellationToken);
+
+ await Assert.That(references.Any(path => path.Contains("ServerShared", StringComparison.Ordinal))).IsTrue();
+ await Assert.That(await project.GetPropertyAsync("IsClientSideProject", cancellationToken)).IsEqualTo("false");
+ }
+
+ [Test]
+ public async Task ServerSharedProject_IsDetectedByName(CancellationToken cancellationToken)
+ {
+ using var project = await CreateAsync("ServerShared", BlazorFixtures.Library(), cancellationToken);
+
+ await Assert.That(await project.GetPropertyAsync("IsServerSharedProject", cancellationToken)).IsEqualTo("true");
+ }
+
+ [Test]
+ public async Task ServerSharedProject_DoesNotReferenceItself(CancellationToken cancellationToken)
+ {
+ using var project = await CreateAsync("ServerShared", BlazorFixtures.Library(), cancellationToken);
+
+ var references = await ReferencesAsync(project, cancellationToken);
+
+ await Assert.That(references).IsEmpty();
+ }
+
+ [Test]
+ public async Task WebAssemblyClient_DoesNotReferenceServerShared(CancellationToken cancellationToken)
+ {
+ using var client = await CreateWithServerSharedSiblingAsync("Shop.Client", string.Empty, cancellationToken);
+
+ await Assert.That(await client.GetPropertyAsync("IsClientSideProject", cancellationToken)).IsEqualTo("true");
+ await Assert
+ .That(
+ (await ReferencesAsync(client, cancellationToken)).Any(path =>
+ path.Contains("ServerShared", StringComparison.Ordinal)
+ )
+ )
+ .IsFalse();
+ }
+
+ [Test]
+ public async Task BrowserSafeLibrary_DoesNotReferenceServerShared(CancellationToken cancellationToken)
+ {
+ using var project = await CreateWithServerSharedSiblingAsync(
+ "Contracts",
+ "true",
+ cancellationToken
+ );
+
+ await Assert.That(await project.GetPropertyAsync("IsClientSideProject", cancellationToken)).IsEqualTo("true");
+ await Assert
+ .That(
+ (await ReferencesAsync(project, cancellationToken)).Any(path =>
+ path.Contains("ServerShared", StringComparison.Ordinal)
+ )
+ )
+ .IsFalse();
+ }
+
+ [Test]
+ public async Task RazorClassLibrary_DoesNotReferenceServerShared(CancellationToken cancellationToken)
+ {
+ var rcl = await BlazorFixtures.CreateAsync(
+ "Widgets",
+ """
+
+
+ net10.0
+ true
+
+
+ """,
+ namespacePrefix: "Acme",
+ cancellationToken: cancellationToken
+ );
+
+ using (rcl)
+ {
+ using var serverShared = await AddSiblingAsync(
+ rcl,
+ "ServerShared",
+ BlazorFixtures.Library(),
+ cancellationToken
+ );
+
+ await Assert.That(await rcl.GetPropertyAsync("IsClientSideProject", cancellationToken)).IsEqualTo("true");
+ await Assert
+ .That(
+ (await ReferencesAsync(rcl, cancellationToken)).Any(path =>
+ path.Contains("ServerShared", StringComparison.Ordinal)
+ )
+ )
+ .IsFalse();
+ }
+ }
+
+ ///
+ /// The conservative Razor-class-library classification is a default, not a verdict: a repository
+ /// that knows the library is server-only can say so and get the automatic reference back.
+ ///
+ [Test]
+ public async Task ExplicitNotClientSide_RestoresTheAutomaticReference(CancellationToken cancellationToken)
+ {
+ var rcl = await BlazorFixtures.CreateAsync(
+ "Widgets",
+ """
+
+
+ net10.0
+ true
+ false
+
+
+ """,
+ namespacePrefix: "Acme",
+ cancellationToken: cancellationToken
+ );
+
+ using (rcl)
+ {
+ using var serverShared = await AddSiblingAsync(
+ rcl,
+ "ServerShared",
+ BlazorFixtures.Library(),
+ cancellationToken
+ );
+
+ await Assert
+ .That(
+ (await ReferencesAsync(rcl, cancellationToken)).Any(path =>
+ path.Contains("ServerShared", StringComparison.Ordinal)
+ )
+ )
+ .IsTrue();
+ }
+ }
+
+ ///
+ /// A Shared project is what a browser-bound project most often pulls in, so it must never inherit a
+ /// server-only reference automatically.
+ ///
+ [Test]
+ public async Task SharedProject_DoesNotReferenceServerShared(CancellationToken cancellationToken)
+ {
+ using var shared = await CreateWithServerSharedSiblingAsync("Shared", string.Empty, cancellationToken);
+
+ await Assert
+ .That(
+ (await ReferencesAsync(shared, cancellationToken)).Any(path =>
+ path.Contains("ServerShared", StringComparison.Ordinal)
+ )
+ )
+ .IsFalse();
+ }
+
+ [Test]
+ public async Task AutomaticReference_CanBeDisabled(CancellationToken cancellationToken)
+ {
+ using var project = await CreateWithServerSharedSiblingAsync(
+ "Api",
+ "false",
+ cancellationToken
+ );
+
+ await Assert
+ .That(
+ (await ReferencesAsync(project, cancellationToken)).Any(path =>
+ path.Contains("ServerShared", StringComparison.Ordinal)
+ )
+ )
+ .IsFalse();
+ }
+
+ [Test]
+ public async Task ClientReferencingServerShared_Warns(CancellationToken cancellationToken)
+ {
+ using var project = await CreateAsync(
+ "Shop.Client",
+ BlazorFixtures.Library(items: """"""),
+ cancellationToken
+ );
+ using var serverShared = await AddSiblingAsync(
+ project,
+ "ServerShared",
+ BlazorFixtures.Library(),
+ cancellationToken
+ );
+
+ var (code, output, errors) = await project.RunMSBuildAsync(
+ "-t:PurviewValidateServerSharedReferences",
+ cancellationToken
+ );
+
+ await Assert.That(code).IsZero().Because(TestHelpers.GenerateError(output, errors));
+ await Assert.That(output).Contains("warning PRSHR0001");
+ await Assert.That(output).Contains("'ServerShared'");
+ }
+
+ [Test]
+ public async Task SharedReferencingServerShared_Warns(CancellationToken cancellationToken)
+ {
+ using var project = await CreateAsync(
+ "Shared",
+ BlazorFixtures.Library(items: """"""),
+ cancellationToken
+ );
+ using var serverShared = await AddSiblingAsync(
+ project,
+ "ServerShared",
+ BlazorFixtures.Library(),
+ cancellationToken
+ );
+
+ var (code, output, errors) = await project.RunMSBuildAsync(
+ "-t:PurviewValidateServerSharedReferences",
+ cancellationToken
+ );
+
+ await Assert.That(code).IsZero().Because(TestHelpers.GenerateError(output, errors));
+ await Assert.That(output).Contains("warning PRSHR0002");
+ }
+
+ [Test]
+ public async Task ServerSharedReferenceCheck_AsError_Fails(CancellationToken cancellationToken)
+ {
+ using var project = await CreateAsync(
+ "Shop.Client",
+ BlazorFixtures.Library(
+ "true",
+ items: """"""
+ ),
+ cancellationToken
+ );
+ using var serverShared = await AddSiblingAsync(
+ project,
+ "ServerShared",
+ BlazorFixtures.Library(),
+ cancellationToken
+ );
+
+ var (code, output, errors) = await project.RunMSBuildAsync(
+ "-t:PurviewValidateServerSharedReferences",
+ cancellationToken
+ );
+
+ await Assert.That(code).IsNotZero();
+ await Assert.That(output).Contains("error PRSHR0001");
+ }
+
+ ///
+ /// The documented escape hatch is the existing browser-safe acceptance: a reference that says it was
+ /// judged safe is not a decision this check gets to second-guess.
+ ///
+ [Test]
+ public async Task ServerSharedReference_DeclaredBrowserSafe_DoesNotWarn(CancellationToken cancellationToken)
+ {
+ using var project = await CreateAsync(
+ "Shop.Client",
+ BlazorFixtures.Library(
+ items: """"""
+ ),
+ cancellationToken
+ );
+ using var serverShared = await AddSiblingAsync(
+ project,
+ "ServerShared",
+ BlazorFixtures.Library(),
+ cancellationToken
+ );
+
+ var (code, output, errors) = await project.RunMSBuildAsync(
+ "-t:PurviewValidateServerSharedReferences",
+ cancellationToken
+ );
+
+ await Assert.That(code).IsZero().Because(TestHelpers.GenerateError(output, errors));
+ await Assert.That(output).DoesNotContain("PRSHR0001");
+ await Assert.That(output).DoesNotContain("PRSHR0002");
+ }
+
+ [Test]
+ public async Task Check_CanBeDisabled(CancellationToken cancellationToken)
+ {
+ using var project = await CreateAsync(
+ "Shop.Client",
+ BlazorFixtures.Library(
+ "true",
+ items: """"""
+ ),
+ cancellationToken
+ );
+ using var serverShared = await AddSiblingAsync(
+ project,
+ "ServerShared",
+ BlazorFixtures.Library(),
+ cancellationToken
+ );
+
+ var (code, output, errors) = await project.RunMSBuildAsync(
+ "-t:PurviewValidateServerSharedReferences",
+ cancellationToken
+ );
+
+ await Assert.That(code).IsZero().Because(TestHelpers.GenerateError(output, errors));
+ await Assert.That(output).DoesNotContain("PRSHR0001");
+ }
+
+ [Test]
+ public async Task RootNamespace_StripsTheServerSharedSuffix(CancellationToken cancellationToken)
+ {
+ using var project = await CreateAsync("ServerShared", BlazorFixtures.Library(), cancellationToken);
+
+ await Assert.That(await project.GetPropertyAsync("RootNamespace", cancellationToken)).IsEqualTo("Acme");
+ }
+}
diff --git a/src/tests/BuildSdk.IntegrationTests/TargetFrameworkSetTests.cs b/src/tests/BuildSdk.IntegrationTests/TargetFrameworkSetTests.cs
index acd4fe5..391466f 100644
--- a/src/tests/BuildSdk.IntegrationTests/TargetFrameworkSetTests.cs
+++ b/src/tests/BuildSdk.IntegrationTests/TargetFrameworkSetTests.cs
@@ -6,12 +6,12 @@ namespace Purview.BuildSdk;
///
/// Verifies the curated PurviewTargetFrameworkSet selection: a repository names a set instead of
/// hard-coding a TFM list in every project, so a lifecycle change is one SDK bump rather than an edit in
-/// every consuming repository. The feature is opt-in, and every narrower declaration still wins.
+/// every consuming repository. Current is always in play, and every narrower declaration still wins.
///
sealed class TargetFrameworkSetTests
{
[Test]
- public async Task NoSetSelected_KeepsTheSingleTargetFrameworkDefault(CancellationToken cancellationToken)
+ public async Task NoSetSelected_UsesTheDefaultMultiTargetSet(CancellationToken cancellationToken)
{
using var h = await ProjectHarness
.For("Library")
@@ -25,13 +25,14 @@ public async Task NoSetSelected_KeepsTheSingleTargetFrameworkDefault(Cancellatio
)
.BuildAsync(cancellationToken);
- // An SDK upgrade must never change a package's targets on its own.
+ // 'Current' is the default set and now covers both releases still in support.
var properties = await h.GetPropertiesAsync(cancellationToken, "TargetFramework", "TargetFrameworks");
- await Assert.That(properties["TargetFramework"]).IsEqualTo("net10.0");
- await Assert.That(properties["TargetFrameworks"]).IsEmpty();
+ await Assert.That(properties["TargetFramework"]).IsEmpty();
+ await Assert.That(properties["TargetFrameworks"]).IsEqualTo("net10.0;net11.0");
}
[Test]
+ [Arguments("Current", "net10.0;net11.0")]
[Arguments("Supported", "net10.0;net11.0")]
[Arguments("Broad", "net8.0;net9.0;net10.0")]
[Arguments("All", "net8.0;net9.0;net10.0;net11.0")]
@@ -59,7 +60,6 @@ CancellationToken cancellationToken
}
[Test]
- [Arguments("Current", "net10.0")]
[Arguments("Latest", "net11.0")]
public async Task SingleTargetSet_ResolvesToTargetFramework(
string set,
@@ -375,6 +375,7 @@ public async Task UnrecognisedSet_FailsTheBuild(CancellationToken cancellationTo
/// Neither dotnet nor msbuild reproduces it - both only evaluate the outer build.
///
[Test]
+ [Arguments("Current", "net10.0;net11.0", "net10.0")]
[Arguments("Supported", "net10.0;net11.0", "net10.0")]
[Arguments("All", "net8.0;net9.0;net10.0;net11.0", "net8.0")]
public async Task MultiTargetSet_ResolvesIdenticallyUnderAnInnerBuildTargetFramework(
diff --git a/templates/global.json b/templates/global.json
index 77535b3..739d4fb 100644
--- a/templates/global.json
+++ b/templates/global.json
@@ -3,6 +3,6 @@
"runner": "Microsoft.Testing.Platform"
},
"msbuild-sdks": {
- "Purview.BuildSdk": "1.3.2"
+ "Purview.BuildSdk": "2.0.0"
}
}