A reusable MSBuild SDK NuGet package that delivers standardised .NET project defaults, code-style enforcement, test-framework wiring, and Central Package Management integration. Install it once per repo — every project beneath the repo root inherits everything automatically.
Note
This SDK package imposes convention over configuration, enforcing certain styles and automations based on project file names, etc.
| Feature | Detail |
|---|---|
| Project type detection | IsCSharpProject, IsTestProject, IsSharedTestingProject, IsServerSharedProject, IsClientSideProject, IsContainerProject, IsWebSdkProject, IsAspireHostProject, IsBlazorWebAssemblyProject, IsBffProject, … |
| .NET 11 opt-ins | A net11.0 target can opt into the C# 15 updated memory-safety rules (MemorySafetyRules=2) and Runtime Async (PurviewRuntimeAsync=true): both are off by default, and AllowUnsafeBlocks stays at its compiler default (false); builds use MSBuild's multithreaded mode through a bootstrapped Directory.Build.rsp (see .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) |
| 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) |
| C# defaults | net10.0;net11.0 TFMs (overridable), LangVersion=latest (C# 15), Nullable=enable, ImplicitUsings=enable, TreatWarningsAsErrors=true (every project; set false to opt out), 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, global.json and (on a .NET 11 or later SDK) Directory.Build.rsp are auto-created by default (disable via DisableAutoCopySdkFiles=true) |
| CI detection | ContinuousIntegrationBuild set automatically when the CI, GITHUB_ACTIONS or TF_BUILD environment variable is true |
| SourceLink | Microsoft.SourceLink.GitHub added to every non-test project (PrivateAssets="all"), configurable for Azure DevOps, GitLab, Bitbucket and more via SourceLinkPackageName (see SourceLink) |
| 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, .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 |
| Version detection | Reads version from package.json and applies it to Version and PackageVersion automatically; falls back to 0.0.1 |
| CPM | ManagePackageVersionsCentrally=true — versions live in your Directory.Packages.props |
{
"test": {
"runner": "Microsoft.Testing.Platform"
},
"msbuild-sdks": {
"Purview.BuildSdk": "2.1.0"
}
}<Project>
<PropertyGroup>
<!-- Required: sets the root namespace prefix for all projects -->
<NamespacePrefix>YourCompany</NamespacePrefix>
</PropertyGroup>
<Import Sdk="Purview.BuildSdk" Project="Sdk.props" />
</Project><Project>
<Import Sdk="Purview.BuildSdk" Project="Sdk.targets" />
</Project>Copy templates/Directory.Packages.props from the repository to your repo root. The templates are not packed into the NuGet package — they are starting points held in git, so take them from there. Every package names a version. A PackageVersion is a minimum rather than an exact pin — NuGet resolves it unless something in the graph requires higher — so floating versions are not needed to stay current, and they would make a build depend on the day it ran.
Note:
ManagePackageVersionsCentrally=trueis set by the SDK. You must have aDirectory.Packages.propsat your repo root for CPM to work, even if it only contains the packages the SDK adds automatically.
Four things have to be right before the SDK behaves as documented:
| What | Where | Notes |
|---|---|---|
NamespacePrefix |
Directory.Build.props, above the import |
Required. The build fails without it (ValidateRootNamespacePrefixTarget) unless DisableNamespacePrefixCheck=true. |
| The SDK version | global.json → msbuild-sdks.Purview.BuildSdk |
Pinned to a released version, e.g. 2.0.1. The SDK bootstraps the file when it is missing and records the version it was packed as (PurviewBuildSdkVersionForGlobalJson). |
| The product version | repo-root package.json → version |
Applied to both Version and PackageVersion by default. A git repository with no root package.json fails the build: add one, or set UsePackageJsonVersion=false. UsePackageJsonVersion=Strict also fails when no repository root is found at all, instead of silently falling back to 0.0.1. |
| Package versions | Directory.Packages.props |
Central Package Management is on (ManagePackageVersionsCentrally=true). Every PackageVersion is a minimum, not an exact pin. |
NamespacePrefix, UsePackageJsonVersion and RootPackageJson must be set before the <Import>.
See Version Detection and the
Configuration Reference.
A project that declares neither TargetFramework nor TargetFrameworks gets the SDK's default set,
net10.0;net11.0 (Current). Pick a different one in one of three ways, narrowest first:
- Declare it explicitly —
TargetFrameworkfor a single target,TargetFrameworks(plural) for many. An explicit value always wins over every set, wherever it is set: in the project (with or without aCondition), in a file the project imports, or inDirectory.Build.propsbefore or after the import. OnlyDirectory.Build.targetsis too late — the .NET SDK has already decided whether the project multi-targets by then. - Select a curated set —
<PurviewTargetFrameworkSet>Supported</PurviewTargetFrameworkSet>inDirectory.Build.propsor the.csproj. Named sets areCurrent(the default),Latest,Supported,BroadandAll, so a lifecycle change is one SDK bump rather than an edit in every project. - Redefine a set — set
PurviewTargetFrameworks<Set>(for examplePurviewTargetFrameworksSupported) before the import to pin a repository's meaning of a set.
An unrecognised set name fails the build with PurviewInvalidTargetFrameworkSet. A project declaring
TargetFrameworks keeps TargetFramework empty. A Roslyn component targets netstandard2.0 and
ignores the sets. See
Target framework sets.
Purview.BuildSdk treats naming, placement, and test structure as configuration. Follow the SDK's
engineering conventions so it can infer namespaces, assembly identities, test categories, project
references, and default package wiring without bespoke overrides.
Start with:
- Engineering Principles
- Project Naming Conventions
- Project Type Detection
- Assembly Name Generation
- Testing Wiring
The SDK applies several conventions automatically based on the .csproj filename and NamespacePrefix.
The wiki pages above are the source of truth; this section is intentionally brief.
RootNamespace is derived from $(NamespacePrefix).$(ProjectName) and is the canonical code identity by
default. AssemblyName and PackageId normally follow the resolved project identity. Test projects keep
their detected suffix so test assemblies stay distinct from the source assembly.
Set EnableAssemblyNameGeneration=false before the SDK import to opt out and use standard .NET
project-name behavior.
.csproj filename |
AssemblyName / PackageId |
RootNamespace |
Detected as |
|---|---|---|---|
Api.csproj |
Acme.Api |
Acme.Api |
Source project |
Api.UnitTests.csproj |
Acme.Api.UnitTests |
Acme.Api |
IsTestProject=true, TestingType=Unit |
Api.IntegrationTests.csproj |
Acme.Api.IntegrationTests |
Acme.Api |
IsTestProject=true, TestingType=Integration |
SharedTestingFramework.csproj |
Acme.SharedTestingFramework |
Acme |
IsSharedTestingProject=true |
Note:
InternalsVisibleTofollows$(AssemblyName)— so forApi.csprojthe SDK generatesAcme.Api.UnitTests,Acme.Api.IntegrationTests, etc.
Use short .csproj names - the SDK handles the prefixing:
✅ Api.csproj → short name, SDK resolves the rest
❌ Acme.Api.csproj → redundant prefix, avoid
A build-time check (PurviewProjectFileNameMismatch) enforces that the .csproj filename matches its
parent directory name, preventing inconsistent naming. Set DisableProjectFileNamingConventionCheck=true
to opt out.
The detected test type becomes the baseline test category. Additional categories are still allowed when they improve discoverability.
The default test stack is also convention-driven. Standard test projects automatically receive:
TUnitTUnit.MocksBogus- Microsoft.Testing.Platform integration
Specialised additions such as TUnit.Aspire and Testcontainers stay explicit and intentional.
Prefer an explicit src/ + tests/ split for new repositories. The canonical SDK-friendly patterns are:
- solutions under
src/{SolutionName}.slnx - source projects under
src/src/{ProjectName}/{ProjectName}.csproj - test projects under
src/tests/{ProjectName}.{TestType}Tests/{ProjectName}.{TestType}Tests.csproj
The SDK also supports flatter layouts, but the explicit source/test split is easier to scale and reason about.
# Create a source project
dotnet new classlib -n Api -o src/src/Api
# Create its unit tests
dotnet new classlib -n Api.UnitTests -o src/tests/Api.UnitTests # SDK wires TUnit automatically
# Or flat:
dotnet new classlib -n Api.UnitTests -o Api.UnitTestsFor the full layout, naming, identity, and test-readability conventions, see Engineering Principles and Project Naming Conventions.
The templates/ folder contains ready-to-copy starter files for new repos:
| File | Purpose |
|---|---|
Directory.Build.props |
Bootstrapper — copy to repo root and set NamespacePrefix |
Directory.Build.targets |
Bootstrapper — copy to repo root |
Directory.Packages.props |
A pinned PackageVersion for every package the SDK adds, grouped by version property (TUnitVersion, AspireVersion, …) |
global.json |
msbuild-sdks entry + Microsoft.Testing.Platform test runner |
.gitignore |
ASP.NET Core + VS + Rider + Node combined gitignore |
.gitattributes |
Line-ending normalisation for .cs, .json, .yml, etc. |
.config/dotnet-tools.json |
CSharpier tool manifest |
The SDK packs no skills: they live in a repository's root skills/ folder and are delivered by the
skills CLI from git. Installing is the skills CLI's job and nothing else's — see
Agent Skills.
Repository metadata such as PackageProjectUrl is left to the repository. This repository keeps it in one place with its own PurviewHomepage, PurviewProjectUrl and PurviewDocsUrl properties in src/Directory.Build.props; they are a convention, not SDK properties — the SDK neither defines nor reads them.
.editorconfig and global.json are bootstrapped into the repository root. Every project in a
solution writes those same destinations, so a write is staged into a temporary file and renamed into
place, an existing file is never overwritten unless asked, retries are silent (MSB3026 is demoted
to a low-importance message) and only a copy that still fails after every retry is reported. See
Repository Bootstrap for the retry and failure properties.
The bootstrapped .editorconfig opens with a version marker:
# Purview.BuildSdk 2.1.0 - managed file, do not editThat line is how the SDK recognises a file it wrote. When the SDK version changes, a file carrying
the marker is refreshed so a repository never silently runs a release-old rule set. A .editorconfig
without the marker is yours and is never touched, whatever it contains — which makes deleting the
first line a supported way to take ownership of the file. You then own every rule in it, including the
ones a later SDK version would have fixed.
A repository that adopted the SDK before the marker existed has exactly that file: unmarked, so it is never refreshed, and indistinguishable on disk from one you wrote. Rather than overwrite it, the SDK says so once:
<path>/.editorconfigcarries no Purview.BuildSdk version marker, so the SDK treats it as yours and will never refresh it …
Build once with -p:PurviewRepoBootstrapMode=Always to adopt the SDK's file, or set
PurviewEditorConfigAutoUpdate=false to keep your own and silence it. The warning is reported once
per repository per SDK version — stamped at .purview/editorconfig-adoption.cache, overridable with
PurviewEditorConfigAdoptionStampPath — and is skipped on CI builds, so no advice can fail a build
run with -warnaserror. PurviewEditorConfigAdoptionWarningInCI=true opts back in;
PurviewEditorConfigAdoptionWarning=false turns it off entirely.
The version is stamped in when the package is built, from the version being packed, so it is always
the version you restored. It is not maintained by hand in the repository, and building never rewrites
the tracked .editorconfig: the stamped copy is written to obj/ and that copy is packed.
Do not edit the managed file — put your rules in a second .editorconfig in a subdirectory, most
often src/.editorconfig, and omit root = true:
# src/.editorconfig - yours, never touched by the SDK
[*.cs]
dotnet_diagnostic.CA1062.severity = noneEditorConfig applies the nearest file last, so your settings override the SDK's for everything beneath
that folder, and nothing you write is lost when the managed file is updated. A rule you want to turn
off repository-wide is better expressed as NoWarn in Directory.Build.props, which the managed file
also never touches.
| Property | Effect |
|---|---|
PurviewEditorConfigAutoUpdate=false |
Keep the managed .editorconfig exactly as it is; everything else still bootstraps. |
PurviewEditorConfigAdoptionWarning=false |
Keep an unmarked .editorconfig without being told it will never be refreshed. |
BootstrapEditorConfigToRepoRoot=false |
Do not write a repository .editorconfig at all. |
PurviewRepoBootstrapMode=Never |
Opt out of all bootstrapping, global.json included. |
PurviewRepositoryRootStrategy chooses how the bootstrap locates the repository root:
| Value | Behaviour |
|---|---|
Markers (default) |
The nearest ancestor directory containing .git/, found with plain MSBuild path checks. No process is started. |
Git |
Asks git via rev-parse, falling back to the walk. |
Markers is the default because Git spawns a git process for every project on every build, whether or
not anything needs bootstrapping. The two can disagree: git reports the worktree root, so for a
submodule, a linked worktree, or a package nested inside a larger repository it may answer differently
from "the nearest .git/ above me". Set Git where that distinction matters.
The bootstrap also skips its file-sync task entirely when there is nothing to do — a destination that exists, is not marker-stale, and is not being checked for drift. That avoids compiling the inline task, which is worth roughly 0.7s per build.
PurviewRepoBootstrapMode=Always still forces an overwrite regardless of the marker, and
WarnOnDrift still reports a difference without writing.
Skills are published in the standard Agent Skills format, in a repository's
root skills/ folder, and installed with the standard skills CLI (bun x skills add owner/repo).
The SDK adds nothing bespoke: its targets are a thin wrapper that runs that CLI from the repository
root, once per solution, and keeps the agent directories tidy. Nothing about skills runs during a
restore or a build.
bun x skills add purview-dev/build-sdk # install, the ordinary way
dotnet msbuild <solution> -t:InstallPurviewSkills -p:PurviewSkillsSource=owner/repo # the same, through the SDK
dotnet msbuild <solution> -t:InstallPurviewSkills # restore skills-lock.json
dotnet msbuild <solution> -t:UpdatePurviewSkills # re-add every skills-lock.json source at its latest
dotnet msbuild <solution> -t:CheckPurviewSkills # only the PurviewSkillsAgents directories exist
dotnet msbuild <solution> -t:FixPurviewSkills # remove the CLI's links from any other agent directoryThese run only when named: install, update and fix fail with PRSKL0009 if anything pulls them into
a build, a restore or a design-time build. Append #<ref> to a source to pin it. PurviewSkillsAgents
is empty by default, so the CLI auto-detects agents; set it (for example universal) to choose, and
CheckPurviewSkills/FixPurviewSkills then enforce that only those agents' skill directories exist.
The CLI is launched through a package runner. PurviewSkillsRunner defaults to bun (bun x skills) and also accepts npx, bunx, pnpm, yarn, deno or skills; anything else
falls through to <runner> skills@1. Because skills-lock.json and the agent directories belong to
the repository rather than to a project, every skill target runs from the repository root,
always, and reports PRSKL0003 rather than falling back to a project folder when that root cannot
be resolved. See Agent Skills.
Setting a property in Directory.Build.props before the <Import> always works. A property whose
description says set it before the Sdk.props import only works there; the rest can also be set in a
project file. Every property here is described in the SDK's
property schema, so the same text appears in csproj
IntelliSense.
| Property | Default | Description |
|---|---|---|
UsePackageJsonVersion |
true |
true enables version detection, false disables it, and Strict requires version detection to succeed (build fails if no version source can be resolved). |
RootPackageJson |
(auto-discovered) | Explicit path to a package.json. A relative path resolves from the project directory, so in Directory.Build.props prefer $(MSBuildThisFileDirectory)package.json. |
EnableVersionDetectionCache |
true |
Enables local caching of auto-discovered package.json version results. |
VersionDetectionLogEnabled |
false |
Emits a high-importance message showing the detected package version. Set to true to enable logging. |
VersionDetectionCacheFile |
(one file per project directory under the temporary directory) | File the package.json version detection cache is written to. Set it before the Sdk.props import. |
When UsePackageJsonVersion=true (the default) or UsePackageJsonVersion=Strict, the SDK:
- Explicit path — if
RootPackageJsonis set, reads that file directly. - Auto-discovery — otherwise, finds the repository root (
RepoRoot) and readspackage.jsonfrom there: aRepoRootset before the import, else the root cached for this project, else the CI workspace variables (GITHUB_WORKSPACE,BUILD_SOURCESDIRECTORY,BUILD_REPOSITORY_LOCALPATH,CI_PROJECT_DIR), else the nearest.gitand then the nearestpackage.jsonabove the project.
The first "version" value in the file is applied to both Version and PackageVersion. The build fails when an explicit RootPackageJson does not exist, when a repository root is found with no package.json in it, or when the file has no version. With UsePackageJsonVersion=Strict, it also fails when no repository root can be discovered at all; in non-strict mode that case silently keeps the 0.0.1 default. See Version Detection.
Version detection logging is disabled by default. Set VersionDetectionLogEnabled to true to emit a high-importance message showing the detected package version.
Important — set before the import:
UsePackageJsonVersion,RootPackageJson,RepoRoot,EnableVersionDetectionCacheandVersionDetectionCacheFilemust be set before the<Import Sdk="Purview.BuildSdk" Project="Sdk.props" />line in yourDirectory.Build.props. The version logic runs during that import and cannot see properties set afterwards (e.g. in individual.csprojfiles).<Project> <PropertyGroup> <NamespacePrefix>Acme</NamespacePrefix> <!-- Set here, before the import --> <RootPackageJson>$(MSBuildThisFileDirectory)package.json</RootPackageJson> </PropertyGroup> <Import Sdk="Purview.BuildSdk" Project="Sdk.props" /> </Project>
| Property | Default | Description |
|---|---|---|
NamespacePrefix |
(required) | Root namespace prefix, e.g. Acme. Results in Acme.MyProject. |
DisableNamespacePrefixCheck |
false |
Set to true to suppress the build error for missing NamespacePrefix. |
DisablePurviewStylePolicyValidation |
false |
Set to true to stop the build failing (PRSGD0006-PRSGD0009) when the repository .editorconfig overrides the modifier policy, hides IDE0040/IDE1006, disables the Style category in bulk, weakens the _camelCase field-naming rule, or adds the accessibility rules to NoWarn. |
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 (in the project or Directory.Build.props) to keep the test packages' executable/test-host shape instead. |
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 |
false (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. |
SourceLinkPackageName |
Microsoft.SourceLink.GitHub |
SourceLink provider package, added to every non-test project. Match the repository host: Microsoft.SourceLink.AzureRepos.Git for Azure DevOps Services, Microsoft.SourceLink.AzureDevOpsServer.Git for on-premises Azure DevOps Server / TFS, Microsoft.SourceLink.GitLab, Microsoft.SourceLink.Bitbucket.Git. Set it in Directory.Build.props so every project agrees. See SourceLink. |
DisableSourceLink |
false |
Set to true to stop the SDK from adding the SourceLink package at all. |
EnableAssemblyNameGeneration |
true |
When true (default), AssemblyName and PackageId derive from the fully evaluated RootNamespace. When explicitly false, standard .NET behaviour applies ($(MSBuildProjectName)). Explicit <AssemblyName>/<PackageId> in a .csproj always take precedence. |
DisableProjectFileNamingConventionCheck |
false |
Set to true to disable the validation that requires MyProject\MyProject.csproj naming alignment. |
DisableGenerateAssemblyInfoClass |
false (true for test and shared-testing projects) |
Set to true to disable the generated AssemblyInfo helper source. |
DisableGenerateAssemblyIdentifierClass |
false (true for test and shared-testing projects) |
Set to true to disable the generated assembly identifier marker class. |
AssemblyIdentifierClassName |
<project name minus NamespacePrefix, dots removed>Assembly |
Overrides the name of the generated assembly identifier marker class. |
AutoIncludeUsings |
true |
Controls SDK-added global usings for NamespacePrefix and RootNamespace. |
MemorySafetyRules |
0 |
C# 15 memory-safety tier for net11.0+ targets: 0 off (default), 1 preview pointer syntax, 2 updated rules (updated-memory-safety-rules). The updated rules are an experimental compiler feature, so they are opt-in. AllowUnsafeBlocks is never changed. |
PurviewRuntimeAsync |
(unset, which is treated as false) | Set to true to compile net11.0+ targets with <Features>runtime-async=on</Features>. UseRuntimeAsync=true also opts in; an explicit false under either name keeps it off. |
IsClientSideProject |
(detected) | true for a project whose assemblies are downloaded to a browser — a WebAssembly client, PurviewBrowserSafe library, Razor class library, or *.Client/*.ClientShared/*.Browser/*.Wasm/*.WebAssembly/*.Components (or ClientShared/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 |
(unset, which is treated as false) | Suppresses PRSHR0001/PRSHR0002. |
PurviewServerSharedReferenceCheckAsError |
false |
Makes PRSHR0001/PRSHR0002 errors instead of warnings. |
DisableUnconditionalProjectDeclarationCheck |
false |
Suppresses PurviewConditionedProjectDeclaration, raised when IsRoslynComponent, IsRoslynComponentOnly, IsPackable, PurviewTargetFrameworkSet or RootNamespace is declared only under a Condition the SDK cannot evaluate. |
AutoGeneratedAssemblyInfoFile |
GeneratedAssemblyInfo.cs |
File name of the generated AssemblyInfo helper source, written under the intermediate Properties/ folder. |
AutoGeneratedAssemblyIdentifierFile |
GeneratedAssemblyIdentifier.cs |
File name of the generated assembly identifier source, written under the intermediate Properties/ folder. |
CurrentYear |
(the current year at build time) | Year used in generated assembly metadata. |
For projects where IsPackable=true, the SDK provides these defaults only when the consuming project has not supplied a value — explicit values are always preserved:
| Property | Default | Description |
|---|---|---|
GenerateDocumentationFile |
true |
Emits XML documentation. |
IncludeSymbols |
true |
Produces a symbol package (false for Roslyn-component-only packages — their PDB ships inside the .nupkg under analyzers/dotnet/cs/ via PurviewPackAnalyzerPdb=true). |
SymbolPackageFormat |
snupkg |
Symbol package format. Always the modern .snupkg; the legacy .symbols.nupkg is never produced by default. |
PublishRepositoryUrl |
true |
Publishes the repository URL. |
EmbedUntrackedSources |
true |
Embeds untracked sources for SourceLink. |
DebugType |
portable |
Ensures portable PDBs for symbol-package delivery. |
IncludeSource |
false |
Packs source files into the symbol package. Off for every project, packable or not; SourceLink is how sources reach the debugger. |
Portable PDBs are delivered through the .snupkg; the normal .nupkg does not receive PDB files unless the project explicitly opts in (for example by adding .pdb to AllowedOutputExtensionsInPackageBuildOutputFolder).
Package README: for a packable project with no explicit PackageReadmeFile, the SDK registers PackageReadmeFile=README.md and packs a README to the package root. The project's own README.md, beside its .csproj, wins; otherwise the repository-root README.md (at RepoRoot) is used. The SDK skips this when a README-named file is already being packed — an Sdk/README.md, for example — so no duplicate readme items are produced. No README is required; if neither file exists the pack succeeds without readme metadata.
The SDK never forces organization/package-specific metadata — Authors, Company, PackageLicenseExpression, PackageLicenseFile, Description, PackageTags, PackageProjectUrl, and repository URLs are left to the repository or individual package. IsPackable is not set blindly: it defaults to false and only becomes true when a project explicitly opts in.
Non-packable projects (including web applications) default WarnOnPackingNonPackableProject=false, so solution-wide pack operations skip them silently. Set <WarnOnPackingNonPackableProject>true</WarnOnPackingNonPackableProject> explicitly to re-enable the "cannot be packed" warning.
| Property | Default | Description |
|---|---|---|
DisableAutoCopySdkFiles |
false |
Master switch that disables repo-level SDK file bootstrapping. |
BootstrapEditorConfigToRepoRoot |
true |
Copies the SDK .editorconfig to the repository root when missing. |
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. Left unset, a Blazor WebAssembly client writes -mt:false for the whole repository, because it cannot build under -mt (PRBLZ0002); a value you set always wins. |
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. |
PurviewRepoBootstrapCopyRetryDelayMilliseconds |
500 |
Base delay between bootstrap write attempts. |
PurviewRepoBootstrapCopyFailureAsError |
true |
When false, a failed bootstrap write is a warning instead of an error. |
PurviewSuppressCopyRetryWarnings |
true |
Demotes built-in copy retry notices (MSB3026) to messages. Set to false to see every retry attempt. |
PurviewEditorConfigAutoUpdate |
true |
Refreshes a managed .editorconfig (one opening with the SDK's version marker) when the SDK version changes. false keeps it exactly as it is. |
PurviewEditorConfigAdoptionWarning |
true |
Warns, once per repository per SDK version, when the repository .editorconfig is not one the SDK manages and so will never be updated. |
PurviewEditorConfigAdoptionWarningInCI |
false |
Reports the adoption warning on CI builds too. Off so that advice cannot fail a TreatWarningsAsErrors build. |
PurviewEditorConfigAdoptionStampPath |
(.purview/editorconfig-adoption.cache beside the repository .editorconfig) | Where the once-per-SDK-version record of the adoption warning is kept. |
PurviewRepositoryRootStrategy |
Markers |
How the bootstrap finds the repository root: Markers walks up to the nearest root marker without starting a process; Git asks git for the worktree root. See Finding the repository root. |
EditorConfigFilePath |
(the .editorconfig inside the installed SDK package) | The SDK-provided .editorconfig injected into @(EditorConfigFiles) for build-time style enforcement. Set it before the Sdk.props import. |
RepoRoot |
(discovered from CI workspace variables, then a .git marker or package.json above the project) | Repository root used for version detection and bootstrapping. Set it before the Sdk.props import. |
| Property | Default | Description |
|---|---|---|
PurviewAutoSdkPack |
true |
When true, automatically packs the Sdk/ folder contents into the NuGet package with the correct root-level paths. Disable this for MSBuild SDK projects. |
The SDK writes nothing into an agent directory. It used to mirror a package's
.agents/**into the consuming repository; that is gone, because the skills CLI covers it and two writers on one set of paths meant the mirror could silently overwrite a pinned install. The SDK packs no.agents/**content, and reads none from packages: skills come from git.
| Property | Default | Description |
|---|---|---|
PurviewSkillsLockFile |
<repo root>/skills-lock.json |
Overrides where the skills CLI's lockfile is read from. |
PurviewSkillsSourceFolder |
<repo root>/skills |
Repository folder holding authored skills. |
PurviewSkillsAgents |
(empty, so the skills CLI auto-detects agents) | Comma-separated agents the skill targets install to, and that recommended commands name. When set, CheckPurviewSkills/FixPurviewSkills allow only those agents' skill directories. |
PurviewSkillsDryRun |
(unset, which is treated as false) | When true, InstallPurviewSkills and UpdatePurviewSkills print the CLI commands without running them. |
PurviewSkillsRunner |
bun |
Package runner used to launch the skills CLI (bun, npx, bunx, pnpm, yarn, deno, skills, or any other). |
PurviewSkillsRunnerCandidates |
bun;npx;bunx;pnpm;yarn;deno |
Runners probed on PATH after PurviewSkillsRunner. |
PurviewSkillsPackage |
skills@1 |
Package spec the executed launcher pins to. |
PurviewSkillsInstallRoot |
(resolved) | Overrides the repository root every skill target runs from. |
PurviewRepositoryRootMarkers |
.git/;.github/;.gitattributes;LICENSE.md;AGENTS.md;.agents/;CLAUDE.md;.claude/;package.json |
Ordered markers identifying a repository root. The first is definitive and searched to the top; the rest resolve nearest-first. |
PurviewSkillsCommand |
(detected) | Overrides the launcher entirely and skips runner detection. |
PurviewSkillsSource |
(none) | The source InstallPurviewSkills adds (owner/repo, optionally #ref); without it, install restores skills-lock.json. |
PurviewSkillsRepoRoot |
(RepoRoot, else the nearest folder with an AGENTS.md) | Evaluation-time guess at the repository root for the skill targets; a value you set outranks the marker walk. Set it before the Sdk.props import. |
When a project is packable, the SDK treats any content under Sdk/ as a pack target with these rules:
| Source path | Package path |
|---|---|
Sdk/.github/** |
.github/** |
Sdk/build/** |
build/** |
Sdk/buildTransitive/** |
buildTransitive/** |
Sdk/buildMultiTargeting/** |
buildMultiTargeting/** |
Sdk/*.md, Sdk/*.png, Sdk/*.jpg, etc. |
package root |
everything else under Sdk/ |
Sdk/ |
| Property | Default | Description |
|---|---|---|
ExcludePurviewTelemetry |
false |
Set to true to exclude Purview.Telemetry.SourceGenerator from all projects. |
ExcludeMSTelemetryExtension |
false |
Set to true to exclude Microsoft.Extensions.Telemetry.Abstractions. Only relevant when ExcludePurviewTelemetry is also false — when ExcludePurviewTelemetry=true the whole telemetry group is skipped anyway. |
| Property | Default | Description |
|---|---|---|
TestingFramework |
TUnit |
Testing framework. Supported values: TUnit, Xunit, None. |
SubstituteFramework |
TUnitMocks |
Mocking provider. Supported values: TUnitMocks, NSubstitute, None. |
TestDataFramework |
Bogus |
Test data provider. Supported values: Bogus, None. |
DisableAutoInternalsVisibleTo |
false |
Set to true to disable automatic InternalsVisibleTo generation for test types and shared testing projects. |
An explicit TargetFramework or TargetFrameworks always wins over every set. See
Setting the target framework(s).
| Property | Default | Description |
|---|---|---|
PurviewTargetFrameworkSet |
Current |
Curated set used when no TargetFramework/TargetFrameworks is set: Latest, Current, Supported, Broad or All. Set it before the Sdk.props import, or unconditionally in the project file. An unknown name fails with PurviewInvalidTargetFrameworkSet; a Roslyn component ignores the sets. |
PurviewTargetFrameworksLatest |
net11.0 |
The Latest set: the latest shipped release only. Set it before the Sdk.props import to redefine the set. |
PurviewTargetFrameworksCurrent |
net10.0;net11.0 |
The Current set, the default: the current LTS plus the latest release. Set it before the Sdk.props import to redefine the set. |
PurviewTargetFrameworksSupported |
net10.0;net11.0 |
The Supported set: every release still in support. Set it before the Sdk.props import to redefine the set. |
PurviewTargetFrameworksBroad |
net8.0;net9.0;net10.0 |
The Broad set: every shipped release, without committing to the newest major. Set it before the Sdk.props import to redefine the set. |
PurviewTargetFrameworksAll |
net8.0;net9.0;net10.0;net11.0 |
The All set: Broad plus the newest major. Set it before the Sdk.props import to redefine the set. |
The SDK classifies every project before its body is evaluated. Each result is also
compiler-visible. Override one by setting it before the Sdk.props
import (or, where noted, in the project).
| Property | Default | Description |
|---|---|---|
IsCSharpProject |
(detected from the project file extension) | true for a .csproj; every C# default in the SDK is gated on it. |
IsSdkProject |
(detected from the project) | true when an Sdk value is found on the project or its import declaration. |
SdkProjectName |
(detected from the project) | The SDK named by the project or its import, for example Microsoft.NET.Sdk.Web. |
IsWebProject |
(detected from the project) | Marks a project with web behaviour (a web SDK project or an API host). |
IsWebSdkProject |
(detected from the project) | true when the project SDK is Microsoft.NET.Sdk.Web. |
IsWorkerSdkProject |
(detected from the project) | true when the project SDK is Microsoft.NET.Sdk.Worker. |
IsAspireHostProject |
(detected from the project) | true when the project SDK starts with Aspire.Sdk.Host or Aspire.AppHost.Sdk. |
IsBlazorWebAssemblyProject |
(detected from the project) | true when the project SDK is Microsoft.NET.Sdk.BlazorWebAssembly. |
IsRazorClassLibraryProject |
(detected from the project) | true when the project SDK is Microsoft.NET.Sdk.Razor. |
IsBlazorHostProject |
(detected from the project) | true for a web SDK project that references a Blazor WebAssembly client. Says nothing about whether the host is a BFF. |
IsBffProject |
(detected from the project) | true for a web SDK project whose name ends with Bff; set true or false (in the project too) to override. Gates PDS0008/PDS0009. |
IsCLIProject |
(detected from the project) | true when the project name ends with CLI, Console, CommandLine, QuickStart or QuickStarts. |
IsContainerProject |
(detected from the project) | true when a Dockerfile beside the project selects the container defaults. |
IsSharedProject |
(detected from the project name) | true for a Shared helper project (Shared, SharedFramework, ...), which other projects reference automatically. |
IsServerSharedProject |
(detected from the project name) | true for a ServerShared helper project, which only projects that cannot run in a browser reference automatically. |
IsSharedTestingProject |
(detected from the project name) | true for a shared testing helper project (SharedTestingFramework, ...). |
TestingType |
(detected from the project name) | Test category taken from a test project's name suffix (Unit, Integration, E2E, ...). |
TargetProjectName |
(detected from the project name) | For a test project, the project under test, inferred from the test project's name. |
See Blazor WebAssembly and BFF.
| Property | Default | Description |
|---|---|---|
DisablePurviewBlazorWebAssemblyDefaults |
false |
Keeps the generic defaults on a Blazor WebAssembly client (packable when asked, container detection, the automatic Shared reference). A ServerShared project is never added either way. Set it before the Sdk.props import. |
PurviewAutoBlazorClientReference |
true |
Lets a host named Name reference its Name.Client WebAssembly client automatically. |
PurviewBrowserSafe |
(unset, which is treated as false) | Applies the browser-safe defaults to a class library: IsAotCompatible, SupportedPlatform browser, and trim, AOT and CA1416 diagnostics as errors. |
PurviewBrowserSafeWarningsAsErrors |
(true on a PurviewBrowserSafe library, otherwise unset) | When false, a browser-safe library keeps its trim, AOT and CA1416 diagnostics as warnings. |
PurviewBrowserSafeWarningCodes |
(on a PurviewBrowserSafe library, CA1416, IL2001-IL2128, IL3000-IL3005 and IL3050-IL3059) | Diagnostics a browser-safe library escalates to errors. |
DisablePurviewBrowserSafeReferenceCheck |
(unset, which is treated as false) | Turns off PRBLZ0001, the check that a WebAssembly client's (or browser-safe library's) project references set PurviewBrowserSafe. |
PurviewBrowserSafeReferenceCheckAsError |
(unset, which is treated as false) | Reports PRBLZ0001 as an error instead of a warning. |
PurviewBffExposesApi |
(true when the BFF references Microsoft.AspNetCore.OpenApi, otherwise unset) | Keeps the OpenAPI interceptor namespace on a BFF host. |
| Property | Default | Description |
|---|---|---|
DisableRoslynCompilerDefaultsValidation |
false |
Stops ValidateRoslynComponentCompilerSettings failing the pack of a Roslyn component whose compiler defaults were changed. |
PurviewPackAnalyzerPdb |
(true for an analyzer-only Roslyn component, otherwise unset) | Packs a Roslyn component's PDB beside its analyzer under analyzers/dotnet/cs/, because a .snupkg cannot carry analyzer symbols. |
PurviewSourceGeneratorFrameworkAnalyzerValidation |
(unset, which is treated as true) | Set to false to turn off PRSGD0005, which checks that every file in a Roslyn component's analyzer closure is self-contained. |
PurviewAnalyzerClosurePermittedReferences |
Microsoft.CodeAnalysis;System.Composition;System.Collections.Immutable;System.Memory;System.Threading.Tasks.Extensions;netstandard;mscorlib;System. |
Simple-name prefixes of host-provided assemblies an analyzer-closure file may reference without PRSGD0005. |
PurviewIncludeComponentReferenceAnalyzerClosure |
true |
Includes a referenced Roslyn component's analyzer artifact in the referencing component's analyzer closure. |
PurviewImportProjectReferenceBuildTransitive |
true |
Registers the CompilerVisibleProperty items a referenced project ships in Sdk/buildTransitive, so a package's own properties reach analyzers while developing it through ProjectReference. |
For a package that ships its own .props/.targets and an MSBuild Editor sidecar. See
MSBuild property schema and IntelliSense.
| Property | Default | Description |
|---|---|---|
EnablePurviewPropertySchemaValidation |
false |
Runs ValidatePurviewPropertySchema before Pack, failing it when a .buildschema.json sidecar and the package's .props/.targets disagree (PRPSC0001-PRPSC0004). For a package that ships MSBuild properties. Off, so no ordinary build or pack changes. |
PurviewPropertySchemaDirectory |
(the project's Sdk/ folder, which PurviewAutoSdkPack ships) | Folder whose .props/.targets files and .buildschema.json sidecars ValidatePurviewPropertySchema checks. |
PurviewPropertySchemaIgnoredProperties |
(empty) | Compiler-visible properties ValidatePurviewPropertySchema does not require a sidecar entry for, because the package does not own them (Version, RootNamespace, ...). |
The SDK exports its properties via CompilerVisibleProperty, so analyzers and source generators read them through build_property.<PropertyName>:
context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.IsBffProject", out var isBff);Read the exported property rather than re-deriving SDK behaviour — it is the SDK's own answer, and it stays correct when the rules change. A property an analyzer needs must be listed as a CompilerVisibleProperty item (with a <Description>) in Sdk.props; nothing else exposes an MSBuild property to the compiler. The SDK's own analyzers rely on this (for example build_property.IsBffProject gates PDS0008/PDS0009).
| Property | Description |
|---|---|
UsePackageJsonVersion |
Whether version detection from package.json is active. |
RootPackageJson |
Resolved path to the package.json used for version detection. |
RepoRoot |
Repo root directory found via .git auto-discovery. |
Version |
Package/assembly version, sourced from package.json when detection is enabled. |
PackageVersion |
NuGet package version, sourced from package.json when detection is enabled. |
ProjectDir |
Absolute project directory path (with trailing separator) used by analyzers for project-relative file calculations. |
RootNamespace |
Effective project root namespace used by analyzers that validate or customize namespace conventions. |
NamespacePrefix |
Required namespace prefix used to derive RootNamespace. |
DisableNamespacePrefixCheck |
Disables the build error for missing NamespacePrefix. |
TestingFramework |
Selected testing framework (TUnit, Xunit, or None). |
SubstituteFramework |
Selected mocking provider (TUnitMocks, NSubstitute, or None). |
TestDataFramework |
Selected test data provider (Bogus or None). |
SourceLinkPackageName |
SourceLink package ID added by the SDK. |
DisableSourceLink |
Disables automatic SourceLink integration. |
ExcludePurviewTelemetry |
Opt-out for Purview.Telemetry.SourceGenerator. |
ExcludeMSTelemetryExtension |
Opt-out for Microsoft.Extensions.Telemetry.Abstractions. |
PurviewSkillsSourceFolder |
Repository folder holding authored agent skills (default <repo root>/skills). |
PurviewSkillsAgents |
Comma-separated agents the skill targets install to (default empty: the CLI auto-detects). |
PurviewSkillsDryRun |
When true, InstallPurviewSkills and UpdatePurviewSkills print commands without running them. |
PurviewSkillsCommand |
Overrides the launcher entirely and skips runner detection. |
PurviewSkillsSource |
The source InstallPurviewSkills adds (owner/repo, optionally #ref); without it, install restores skills-lock.json. |
PurviewSkillsLockFile |
Where the skills CLI's skills-lock.json is read from (default <repo root>/skills-lock.json). |
PurviewSkillsInstallRoot |
Overrides the repository root every skill target runs from. |
PurviewRepositoryRootMarkers |
Ordered markers identifying a repository root (default .git/;.github/;.gitattributes;LICENSE.md;AGENTS.md;.agents/;CLAUDE.md;.claude/;package.json). |
PurviewSkillsRunner |
Package runner used to launch the skills CLI (default bun; also npx, bunx, pnpm, yarn, deno, skills). |
PurviewSkillsRunnerCandidates |
Runners probed on PATH after PurviewSkillsRunner (default bun;npx;bunx;pnpm;yarn;deno). |
PurviewSkillsPackage |
Package spec the executed launcher pins to (default skills@1). |
PurviewRepoBootstrapMode |
Controls repo file bootstrapping: IfMissing (default), Always, WarnOnDrift or Never. |
PurviewRepoBootstrapCopyRetries |
Number of write attempts before a bootstrap failure is reported (default 3). |
PurviewRepoBootstrapCopyRetryDelayMilliseconds |
Base delay between bootstrap write attempts (default 500). |
PurviewRepoBootstrapCopyFailureAsError |
When true (default), a bootstrap write that still fails after every retry is an error. |
PurviewSuppressCopyRetryWarnings |
When true (default), built-in copy retry notices (MSB3026) are demoted to messages. |
PurviewAutoSdkPack |
When true, automatically packs the Sdk/ folder contents into the NuGet package with the correct root-level paths. |
DisableGenerateAssemblyInfoClass |
Disables generated AssemblyInfo helper source. |
DisableGenerateAssemblyIdentifierClass |
Disables the generated assembly identifier marker class. |
EnableAssemblyNameGeneration |
When true (default), AssemblyName derives from RootNamespace. |
DisableAutoInternalsVisibleTo |
Disables automatic InternalsVisibleTo generation. |
AutoIncludeUsings |
Controls SDK-added global usings. |
IsCSharpProject |
True when the project is a .csproj. |
IsTestProject |
True when project name ends with a supported test suffix. |
IsSharedTestingProject |
True for known shared testing helper project names. |
TestingType |
Detected test category suffix from project name. |
TargetProjectName |
Inferred target project name for test projects. |
IsContainerProject |
True when Dockerfile markers indicate container defaults. |
IsSdkProject |
True when an SDK value is detected from project/import declaration. |
SdkProjectName |
Detected SDK name (e.g. Microsoft.NET.Sdk.Web). |
IsWebProject |
Marker used in SDK web-project behaviour. |
IsWebSdkProject |
True when SdkProjectName is Microsoft.NET.Sdk.Web. |
IsWorkerSdkProject |
True when SdkProjectName is Microsoft.NET.Sdk.Worker. |
IsAspireHostProject |
True when SDK starts with Aspire.Sdk.Host or Aspire.AppHost.Sdk. |
IsBlazorWebAssemblyProject |
True when SdkProjectName is Microsoft.NET.Sdk.BlazorWebAssembly. |
IsRazorClassLibraryProject |
True when SdkProjectName is Microsoft.NET.Sdk.Razor. |
IsBlazorHostProject |
True for a web SDK project that references a Blazor WebAssembly client. |
IsBffProject |
True for a web SDK project named *Bff, or that sets IsBffProject=true. |
PurviewBrowserSafe |
When true on a class library, applies the browser-safe defaults. |
PurviewBrowserSafeWarningsAsErrors |
When false, browser-safe trim and AOT diagnostics fall back to the project's own warning policy (TreatWarningsAsErrors, true by default). |
DisablePurviewBrowserSafeReferenceCheck |
Turns off PRBLZ0001. |
PurviewBrowserSafeReferenceCheckAsError |
Reports PRBLZ0001 as an error. |
DisablePurviewBlazorWebAssemblyDefaults |
Keeps the generic defaults on a WebAssembly client. |
PurviewAutoBlazorClientReference |
When false, a host does not reference its <Name>.Client automatically. |
PurviewBffExposesApi |
Whether a BFF keeps the OpenAPI interceptor namespace. |
PurviewAutoServerSharedReference |
When false, a server-side project does not automatically reference a sibling ServerShared project. Client-side projects never get one. Defaults to true. |
DisablePurviewServerSharedReferenceCheck |
When true, a client-side or Shared project does not report a ServerShared project reference (PRSHR0001/PRSHR0002). |
PurviewServerSharedReferenceCheckAsError |
When true, PRSHR0001/PRSHR0002 are errors instead of warnings. Defaults to false. |
MemorySafetyRules |
The C# 15 memory-safety tier: 0 off (default), 1 preview syntax, 2 updated rules. The updated rules are an experimental compiler feature, so they are opt-in. AllowUnsafeBlocks is left unset. |
PurviewRuntimeAsync |
When true, a net11.0+ project compiles with the runtime-async compiler feature. Defaults to false; UseRuntimeAsync=true also opts in. |
PurviewMultiThreadedBuild |
When true (default), boots a Directory.Build.rsp that enables MSBuild's multithreaded build (-mt). Left unset, a Blazor WebAssembly client writes -mt:false instead. |
EditorConfigFilePath |
Path to the SDK-provided .editorconfig that is injected into @(EditorConfigFiles). |
RepositoryEditorConfigFilePath |
Destination path for bootstrapping a physical repo-level .editorconfig (defaults to git repo root; falls back to Directory.Build.props directory). |
BootstrapEditorConfigToRepoRoot |
When true (default), copies the SDK .editorconfig to RepositoryEditorConfigFilePath if missing. |
RepositoryGlobalJsonFilePath |
Destination path for bootstrapping a physical repo-level global.json (defaults to git repo root; falls back to Directory.Build.props directory). |
BootstrapGlobalJsonToRepoRoot |
When true (default), creates global.json at RepositoryGlobalJsonFilePath if missing. |
RepositoryDirectoryBuildRspFilePath |
Destination path for bootstrapping a repository-level Directory.Build.rsp when missing. |
BootstrapMultiThreadedBuildRspToRepoRoot |
When true, bootstraps a managed Directory.Build.rsp that enables MSBuild's multithreaded build (-mt). |
DisableAutoCopySdkFiles |
When true, disables SDK auto-copy/bootstrap for repo files (.editorconfig, global.json, Directory.Build.rsp). |
PurviewBuildSdkVersionForGlobalJson |
Version used for msbuild-sdks.Purview.BuildSdk when bootstrapping global.json (auto-detected from SDK package path, falling back to the version stamped in at pack time). |
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). |
AssemblyIdentifierClassName |
Name of the generated assembly identifier marker class. |
IsCLIProject |
True when the project name ends with CLI, Console, CommandLine, QuickStart or QuickStarts. |
IsSharedProject |
True when the project is a shared project. |
IsServerSharedProject |
True for ServerShared helper projects (ServerSharedFramework, etc.), which are referenced automatically only by projects that cannot run in a browser. |
IsClientSideProject |
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, *.ClientShared, *.Browser, *.Wasm, *.WebAssembly, *.Components, or exactly ClientShared or Components). Set explicitly to override. |
LangVersion |
C# language version used to build the Roslyn component. Defaults to latest. |
Nullable |
Nullable reference-types context used to build the Roslyn component. Defaults to enable. |
TreatWarningsAsErrors |
Treats all compiler, analyzer and NuGet warnings as errors. Defaults to true for every project; set false to opt out. |
EnforceExtendedAnalyzerRules |
Enforces Roslyn analyzer best-practice rules. Defaults to true for Roslyn components. |
Deterministic |
Produce deterministic builds. Defaults to true. |
ContinuousIntegrationBuild |
True when building in a recognized CI environment, producing deterministic SourceLink output. |
EmbedUntrackedSources |
Embeds untracked source files for SourceLink. Defaults to true for Roslyn components. |
PurviewSourceGeneratorFrameworkAnalyzerValidation |
False to opt out of PRSGD0005 self-contained analyzer-closure validation. |
<Project>
<PropertyGroup>
<NamespacePrefix>Acme</NamespacePrefix>
<TestingFramework>Xunit</TestingFramework>
<SubstituteFramework>NSubstitute</SubstituteFramework>
<TestDataFramework>None</TestDataFramework>
</PropertyGroup>
<Import Sdk="Purview.BuildSdk" Project="Sdk.props" />
</Project>Every property in the Configuration reference is described in
Sdk.props.buildschema.json, an MSBuild Editor
sidecar packed beside Sdk.props. Install the extension in Visual Studio and a project file that
imports the SDK gets completion, descriptions, defaults and — for TestingFramework,
PurviewTargetFrameworkSet and the other enumerations — the allowed values. Without the extension the
file is inert.
The schema is the canonical description, and just properties-check holds the code, the
compiler-visible list and these tables to it: a property with no entry, a default that differs from the
evaluated one, a missing table row or a schema that fails the MSBuild Editor meta-schema each fail a
named test that says what to add and where.
A package that ships its own .props/.targets can check its own sidecar with the opt-in
ValidatePurviewPropertySchema target (EnablePurviewPropertySchemaValidation=true; PRPSC0001–PRPSC0004).
A sidecar must be named after the full file it describes — build/Acme.Kit.props.buildschema.json
beside build/Acme.Kit.props. See Property Schema and the
purview-buildsdk-msbuild-properties skill.
.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 (opt-in) | <Features>runtime-async=on</Features> when enabled |
Enables Runtime Async for a target where the team has decided it is wanted: cleaner live stack traces, lower await overhead, and continuations that skip ExecutionContext when nothing is ambient. It is off by default because it changes how every await compiles; opt in with PurviewRuntimeAsync=true or UseRuntimeAsync=true. An explicit false under either name keeps it off. |
| C# 15 updated memory-safety rules (opt-in) | <Features>updated-memory-safety-rules</Features> when enabled |
unsafe marks requires-unsafe APIs and the compiler records the choice in MemorySafetyRulesAttribute. Off by default because the updated model is an experimental compiler feature; opt in with MemorySafetyRules=2. 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. A repository with a Blazor WebAssembly client gets -mt:false instead: a net11.0 client cannot build under -mt (see WebAssembly client defaults). |
MemorySafetyRules tiers: 0 (off, the default), 1 (preview pointer syntax, sets LangVersion=preview
unless the project named its own version), 2 (updated rules, opt-in). Anything else fails with
PRCFG0001.
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 blanketServerGarbageCollection=trueis 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).
Every non-test project gets a SourceLink provider package with PrivateAssets="all", so a debugger can
step from a consumed package into its source, and the packed .nuspec carries the repository URL and the
exact commit. Test and shared-testing projects are excluded, because their symbols are not published.
| Property | Default | Description |
|---|---|---|
SourceLinkPackageName |
Microsoft.SourceLink.GitHub |
The provider package. Set it in Directory.Build.props to match the repository host. |
DisableSourceLink |
false |
Set to true to stop the SDK adding the package at all. |
Pick the provider for your host:
| Repository host | Provider package |
|---|---|
| GitHub / GitHub Enterprise | Microsoft.SourceLink.GitHub (default) |
Azure DevOps Services / Azure Repos (dev.azure.com, *.visualstudio.com) |
Microsoft.SourceLink.AzureRepos.Git |
| Azure DevOps Server / TFS (on-premises) | Microsoft.SourceLink.AzureDevOpsServer.Git, plus a SourceLinkAzureDevOpsServerGitHost item |
| GitLab | Microsoft.SourceLink.GitLab |
| Bitbucket | Microsoft.SourceLink.Bitbucket.Git |
<!-- Directory.Build.props, above the <Import> -->
<PropertyGroup>
<SourceLinkPackageName>Microsoft.SourceLink.AzureRepos.Git</SourceLinkPackageName>
</PropertyGroup>Central Package Management is on, so the chosen provider also needs a PackageVersion in
Directory.Packages.props — the shipped template declares GitHub, Azure Repos and GitLab.
ContinuousIntegrationBuild is set from the CI environment (CI, GITHUB_ACTIONS, TF_BUILD), never
from a local build. Full detail, including on-premises host configuration and self-hosted providers, is
in SourceLink.
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, *.ClientShared, *.Browser, *.Wasm, *.WebAssembly, *.Components, ClientShared, 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 aServerSharedproject.PRSHR0002— aSharedproject references aServerSharedproject.
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.
Every behaviour here applies only to the project type it names. A project that is not a WebAssembly
client, a Blazor host, a BFF or a PurviewBrowserSafe library is unchanged.
| Signal | Result |
|---|---|
Sdk="Microsoft.NET.Sdk.BlazorWebAssembly" |
IsBlazorWebAssemblyProject=true |
Sdk="Microsoft.NET.Sdk.Razor" |
IsRazorClassLibraryProject=true |
a web SDK project whose name ends with Bff (case-sensitive), or that sets IsBffProject=true |
IsBffProject=true; IsBffProject=false opts out |
a web SDK project that references an IsBlazorWebAssemblyProject project |
IsBlazorHostProject=true |
IsBlazorHostProject never implies IsBffProject: a host that keeps no tokens is not a BFF. For the
<Name> (host) / <Name>.Client (WebAssembly) pairing it is known at evaluation, and the host gets an
automatic reference to the client unless it declares one (PurviewAutoBlazorClientReference=false
opts out). A host referencing a client under another name is resolved during the build:
dotnet msbuild <host> -t:ResolvePurviewBlazorHostProject -getProperty:IsBlazorHostProject.
| Applied | Failure it prevents |
|---|---|
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* 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 |
the managed Directory.Build.rsp written as -mt:false, naming the client |
the WebAssembly SDK's ReadWasmProps failing under -mt with MSB4060, which names nothing that leads back to the response file |
PRBLZ0002 when a net11.0+ client is built under -mt anyway, before PrepareForBuild |
the same failure, when -mt came from the command line, a response file you own, or PurviewMultiThreadedBuild=true |
The multithreaded build. A net11.0 WebAssembly client cannot build under MSBuild's -mt: the
WebAssembly SDK's ReadWasmProps task comes from a custom task factory that cannot run in the
out-of-process task host -mt moves it to. The Directory.Build.rsp is per repository, so a client
decides for it: when PurviewMultiThreadedBuild is unset, a client writes -mt:false and a line naming
itself, and every other project leaves that file alone while the named client's project file exists
(and restores -mt once it is gone). A PurviewMultiThreadedBuild you set always wins, and a file
without the marker line is yours and is never rewritten. A build that is multithreaded anyway stops with
PRBLZ0002, which says whether -mt came from the command line or a response file and how to turn it
off (<PurviewMultiThreadedBuild>false</PurviewMultiThreadedBuild>, or -mt:false). A repository
upgrading from 2.0.x has a managed -mt file the first build cannot escape: that build rewrites it,
stops with PRBLZ0002 saying so, and the next one builds. MSBuild 18 has no property that says -mt is
on, so the check looks at where an inline task runs: -mt always puts it in the out-of-process task
host. A net10.0 client builds under -mt, so the check does not apply to it.
Globalisation, trimming mode and the Blazor runtime settings are left to the Blazor SDK.
DisablePurviewBlazorWebAssemblyDefaults=true (before the SDK import) restores the generic
defaults. The automatic telemetry packages were checked rather than assumed: the generator is
analyzer-only, and its generated code and Microsoft.Extensions.Telemetry.Abstractions publish
trim-clean and run on the WebAssembly runtime (an integration test boots it), so nothing is excluded.
The SDK adds no exporter.
A library a WebAssembly client downloads runs in the browser, where reflection the trimmer cannot
follow and APIs the browser does not support fail at runtime. <PurviewBrowserSafe>true</PurviewBrowserSafe>
on a class library turns those into build failures, applying only what the project has not set:
IsAotCompatible=true, which turns onIsTrimmableand the trim, single-file and AOT analyzers;<SupportedPlatform Include="browser" />, soCA1416reports browser-unsupported APIs such asSystem.Diagnostics.Process;WarningsAsErrorsfor each trim, single-file, AOT andCA1416id (PurviewBrowserSafeWarningCodes), rather thanTreatWarningsAsErrors, so the rest of the project's warning policy is untouched.PurviewBrowserSafeWarningsAsErrors=falseopts out, handing the ids back toTreatWarningsAsErrors(trueby default);CA1416then falls back to the repository's severity (suggestionin the shipped.editorconfig).
File and network I/O are not reported - File works against the browser's in-memory file system and
HttpClient through fetch, so neither is annotated as unsupported - and stay a review obligation.
The client asks every ProjectReference, through an MSBuild call on the referenced project, for its
PurviewBrowserSafe value and reports PRBLZ0001 for each one that does not set it. A browser-safe
library runs the same check over its own references, so the guarantee covers the whole download.
PurviewBrowserSafe="true" on the ProjectReference records the decision for a project that does not
use this SDK; PurviewBrowserSafeReferenceCheckAsError=true makes it an error and
DisablePurviewBrowserSafeReferenceCheck=true turns it off.
- No packages are added: YARP, OpenID Connect and any BFF library (Duende.BFF is commercially licensed) are the application's choice.
- The OpenAPI interceptor namespace is not added, because a BFF that only forwards has no document to
generate. A BFF referencing
Microsoft.AspNetCore.OpenApi, or settingPurviewBffExposesApi=true, keeps it. PDS0008reportsHttpOnly = falseandPDS0009reportsSameSite = Noneon theCookieAuthenticationOptionscookie - not on the antiforgery cookie or the OpenID Connect correlation and nonce cookies, which legitimately differ.
A persisted Data Protection key ring is not checked: it is routinely configured in a shared ServiceDefaults project the BFF's compilation cannot see, so the rule could not be precise. Anti-forgery on forwarded routes stays a review obligation.
Test projects are automatically detected by their suffix. Supported patterns:
MyProject.UnitTests → IsTestProject=true, TestingType=Unit
MyProject.IntegrationTests→ IsTestProject=true, TestingType=Integration
MyProject.E2ETests → IsTestProject=true, TestingType=E2E
Detection is by shape, not by list: any project name ending in .<Word>Tests or .<Word>Test is a test project with TestingType=<Word> — Billing.ReplayTests is TestingType=Replay. A bare .Tests suffix is not (it has no <Word>). The TestType items in Sdk.props are the types InternalsVisibleTo generates friend names for (see InternalsVisibleTo): Acceptance, Accessibility, Approval, Architecture, Benchmark, BlackBox, Capacity, Chaos, Compatibility, Compliance, Component, Concurrency, Configuration, Contract, Deployment, E2E, EndToEnd, Endurance, Environment, Failover, Functional, Fuzz, Globalization, GrayBox, GreyBox, Integration, Interactive, Interoperability, Load, Localization, Migration, Mutation, Penetration, Performance, Property, Recovery, Regression, Resilience, Sanity, Scenario, Security, Smoke, Snapshot, Soak, Spike, Stress, System, Threat, Unit, Upgrade, Usability, Visual, WhiteBox.
Projects named SharedTestingFramework, SharedTestingInfrastructure, SharedTestingInfra, SharedTestingUtilities, SharedTestingUtils, SharedTestingLibrary, SharedTestingLib, or SharedTestingHelpers are treated as shared testing helpers — they get test package references but not the test runner or coverage settings.
The SDK automatically generates [assembly: InternalsVisibleTo("…")] attributes for every non-test C# project. The friend assembly name is derived from the source project's resolved $(AssemblyName), so all naming modes are handled correctly:
- Explicit
<AssemblyName>— if a project sets<AssemblyName>Custom.Assembly</AssemblyName>, the generated attributes useCustom.Assembly.UnitTests,Custom.Assembly.IntegrationTests, etc. - Default —
AssemblyNameisRootNamespace-derived, so fully-qualified names are used (e.g.Acme.MyProject.UnitTests). EnableAssemblyNameGeneration=false— standard .NET behaviour:$(MSBuildProjectName)(e.g.MyProject.UnitTests).
Two categories of friend assemblies are generated:
- TestType variants — for each defined
TestType(Unit,Integration,Architecture,Contract,Functional, …) the SDK emits the$(AssemblyName).{TestType}Testsalias plus the$(TargetProjectName).{TestType}Testsand$(MSBuildProjectName).{TestType}Testsequivalents — each only when it is non-empty and differs from the ones before, so no grant is duplicated — and the attribute resolves regardless of whether naming comes from an explicitAssemblyName, the SDK default, or the raw project name. - SharedTesting projects — one per known shared testing project name (
SharedTestingFramework,SharedTestingInfrastructure, etc.). By default (EnableAssemblyNameGeneration=true) with aNamespacePrefixset, these are prefixed (e.g.Acme.SharedTestingFramework); withEnableAssemblyNameGeneration=falsethe raw name is used.
To disable automatic InternalsVisibleTo generation, set DisableAutoInternalsVisibleTo=true in your project or Directory.Build.props:
<PropertyGroup>
<DisableAutoInternalsVisibleTo>true</DisableAutoInternalsVisibleTo>
</PropertyGroup>When GenerateAssemblyInfoClassTarget writes the SDK-generated AssemblyInfo source, it also emits:
namespace Microsoft.CodeAnalysis
{
sealed partial class EmbeddedAttribute : System.Attribute { }
}This block is guarded by:
#if !PURVIEW_SDK_EXCLUDE_EMBEDDEDAssemblyInfo is emitted with [Microsoft.CodeAnalysis.Embedded], so the project must have a matching Microsoft.CodeAnalysis.EmbeddedAttribute type available at compile time. The SDK emits that attribute to satisfy the reference and to keep generated metadata/source-generator-facing symbols marked as embedded.
Define PURVIEW_SDK_EXCLUDE_EMBEDDED only when your build already provides Microsoft.CodeAnalysis.EmbeddedAttribute from another source; otherwise compilation will fail because the attribute used by generated AssemblyInfo cannot be resolved.
Architecture tests need a handle on each assembly under test. Matching one by name (Assembly.Load("Acme.Billing")) is a string that no compiler checks, so a rename silently empties the test instead of failing it.
GenerateAssemblyIdentifierClassTarget writes an empty marker class into the intermediate output for every production C# project, so the reference becomes a typeof:
// <auto-generated />
/// <summary>
/// Identifies the <c>Acme.Billing</c> assembly to architecture tests.
/// </summary>
public static partial class BillingAssembly
{
}var assembly = typeof(BillingAssembly).Assembly;The class name comes from $(MSBuildProjectName) with a leading NamespacePrefix and every remaining non-identifier character removed, suffixed with Assembly — with NamespacePrefix set to Acme.Billing, Acme.Billing.Core.Abstractions and Core.Abstractions both produce CoreAbstractionsAssembly. Set AssemblyIdentifierClassName to choose a different name.
It is emitted in the global namespace deliberately: a test project that references several assemblies should not need a using per marker. Being partial, a project can add members to it.
Test and shared-testing projects do not get one — the test assembly is the one doing the asserting, so it has nothing to identify. Set DisableGenerateAssemblyIdentifierClass=true to opt a production project out.
Certain suffixes are automatically stripped from RootNamespace to avoid awkward namespace names like Acme.MyProject.Core.Something.
The current built-in stripping set is intentionally broader than the common examples and includes:
ServiceDefaults, Infrastructure, Abstractions, ClientShared, DataAccess, Extensions, Contracts, Framework, Utilities, Helpers, Library, Common, Shared, Infra, Utils, Core, Host, Util, Lib, EF, plus the known shared, server-shared and shared-testing project names (Shared*, ServerShared*, SharedTesting*).
Three groups are deliberately not stripped:
- Roslyn component names —
SourceGenerator,SourceGenerators,SourceGeneration,Generators,Analyzers,CodeFixers,CodeFixes. A component's suffix is part of its identity, and stripping them producedIDE0130across every Roslyn-component repository consuming this SDK. Persistence,AppHostandData— they read as layer noise but are routinely the whole point of the project.- Anything a project opts back in with
<NamespaceRemoveSuffix Include="..." />.
The strip list is authored as the NamespaceRemoveSuffix item type. To keep a suffix the SDK would
otherwise strip, remove the matching entry — this must come after the Sdk.props import, because
the items are declared there and only exist to be removed once the import has evaluated:
<Import Sdk="Purview.BuildSdk" Project="Sdk.props" />
<ItemGroup>
<!-- Keep the Abstractions and Contracts namespaces. -->
<NamespaceRemoveSuffix Remove="Abstractions" />
<NamespaceRemoveSuffix Remove="Contracts" />
</ItemGroup>Only suffixes present in the built-in list need removing; a suffix the SDK does not ship — the Roslyn
component names above among them — is already kept as-is. The items drive the build-time pass only:
Core, EF, Shared, ClientShared, ServiceDefaults and the shared, server-shared and
shared-testing project names are also stripped while Sdk.props evaluates, from a fixed list, so
removing one of those items does not keep it in the evaluated RootNamespace (or in the AssemblyName
derived from it). See Assembly Name Generation.
When a file is placed under a project-root Extensions/ folder, the analyzer intentionally treats
that folder as a namespace reset point.
- Scope: only files where the first project-relative segment is exactly
Extensions - Expected namespace: derived from subfolders under
Extensions/(file name is ignored) RootNamespaceis deliberately ignored for these files
Examples:
| Project-relative file path | Expected namespace |
|---|---|
Extensions/System/StringExtensions.cs |
System |
Extensions/Microsoft/Extensions/Configuration/ConfigurationExtensions.cs |
Microsoft.Extensions.Configuration |
Extensions/TopLevel.cs |
(global namespace) |
To avoid conflicting guidance, IDE0130 is suppressed for files in this root Extensions/ scope.
The shipped .editorconfig also turns off the namespace-conflict diagnostics this convention can
trigger (CA1724, CS0436, CS1591, IDE0005) for files under any Extensions/ folder
([**/Extensions/**]), so extension files never need #pragma suppressions.
Outside this scope, normal IDE0130 behaviour remains unchanged.
A code refactoring (Split extensions class into one class per receiver type) is offered when a
static extensions class targets multiple receiver types. It splits the class into one
<ReceiverType>Extensions class per receiver — for a generic this TBuilder where TBuilder : IHostApplicationBuilder
receiver that becomes HostApplicationBuilderExtensions — and places each new file under
Extensions/<receiver namespace>/ so the namespace convention above stays satisfied. When a
<ReceiverType>Extensions class already exists in the receiver's namespace, the receiver's methods are
merged into it instead of creating a duplicate file.
A second refactoring (Move extensions class to conventional location) is offered when a static
extensions class targets a single receiver type but is not already at its conventional location (or is
at that location but not conventionally named). It re-paths the file to
Extensions/<receiver namespace>/<Type>Extensions.cs and renames the class to the receiver-derived
name (e.g. an IServiceCollection extension becomes ServiceCollectionExtensions and moves to
Microsoft.Extensions.DependencyInjection). When a <Type>Extensions class already exists in the
receiver's namespace it is merged into that class instead of creating a duplicate — every member from
both classes (constants, private helpers, XML docs) is preserved, only members with a matching
signature are skipped. The namespace is fixed, a using for the previous namespace is added to the
moved file, and a using for the new namespace plus the renamed type name are applied to every other
document that references the type, so the move compiles everywhere.
The package ships Purview.BuildSdk.Analyzers.dll (plus a separate code-fix assembly for the IDE)
and adds the analyzer to every C# project as an <Analyzer> item, so the rules surface in both
command-line builds and Visual Studio.
The shipped .editorconfig enforces a modifier/visibility and field-naming policy, and it is
deliberately not silenceable:
dotnet_style_require_accessibility_modifiers = omit_if_default:warning— a declared modifier that matches the language default (privateinside a type,internalat namespace scope,publicinside an interface) is reported byIDE0040and must be removed. Omit the default modifier instead.- Private instance fields must be
_camelCase, nevercamelCaseorPascalCase: the_prefix keeps field access unambiguous, so the noisythis.qualifier is never needed. Private constants and privatestatic readonlyfields are type-level state and stay PascalCase; non-private constants andstatic readonlyfields follow the same PascalCase rule, and local constants stay camelCase. CA1515(public type in an application/test assembly) ships at warning,CA1852(seal internal types) is no longer suppressed for internals exposed throughInternalsVisibleTo, andCA1034is no longer disabled forExtensions/files. Project shapes whose public surface is mandated by a framework are exempt by the SDK itself: Aspire hosts and CLI apps keep their nested public options types, and test projects use the test-context rule set below.- Test and shared-testing projects get a test-context rule set (
PurviewTestContextNoWarn) that exempts the production API-surface rules (CA1002,CA1012,CA1034,CA1047,CA1050,CA1051,CA1062,CA1064,CA1515,CA1707), because test classes and fixtures are legitimately public, test names useMethod_Scenario_Expectation, helpers expose fields and take fixture parameters without null guards, and abstract test bases have public constructors. Everything stylistic still applies to tests (IDE0040, field naming, formatting, theIDE1006naming rules). SetDisablePurviewTestContextRuleSet=trueto enforce the production rules in tests as well, or overridePurviewTestContextNoWarnwith your own semicolon-fenced list before the SDK import. - Shared testing projects stay libraries even though their test packages request the test-host shape,
so their fixtures remain legitimate public API and
CA1515does not apply to them at all. Use<PurviewSharedTestingOutputType>Exe</PurviewSharedTestingOutputType>to opt back into the package-driven executable.
ValidatePurviewStylePolicy runs before every C# compile and fails the build when a repository neuters
the policy:
| Code | Fails when |
|---|---|
PRSGD0006 |
dotnet_style_require_accessibility_modifiers is overridden with anything other than omit_if_default. |
PRSGD0007 |
A policy rule (IDE0040, CA1515, CA1852, CA1034) is downgraded below warning, any policy rule is set to none/silent, or the Style category is disabled in bulk without an explicit IDE0040 severity. |
PRSGD0008 |
IDE0040, CA1515, CA1852, CA1034, CA1012, CA1047, CA1050, CA1051, CA1062, CA1064, or CA1707 is added to NoWarn by the repository. Entries the SDK injects itself (the test-context rule set for test/shared-testing projects and CA1515 for Aspire hosts and CLI apps) are ignored so the SDK's own context-aware defaults stay usable. |
PRSGD0009 |
IDE1006 is hidden, the private instance field rule is downgraded below warning, or the _ prefix is removed from the field naming style. |
The validator walks the same .editorconfig chain as the compiler (nearest file wins, later entries
beat earlier ones, root = true stops the walk). Because a repository can keep an older bootstrapped
copy of the shipped file, a stale copy is reported rather than obeyed: refresh it with
PurviewRepoBootstrapMode=Always (or delete it), or opt out per repository/project with
DisablePurviewStylePolicyValidation=true.
| Rule | Category | Severity | Description |
|---|---|---|---|
PDS0001 |
(suppressor) | — | Suppresses CS1591 for EditorBrowsable(Never) members |
PDS0002 |
Naming | Warning | Files under a project-root Extensions/ folder reset their namespace |
PDS0003 |
Style | Warning | Prefer an explicit type with target-typed new() over var |
PDS0004 |
Naming | Warning | Use correct acronym capitalization (Api → API) |
PDS0005 |
(suppressor) | — | Suppresses IDE0130 for files under a project-root Extensions/ folder, whose namespace is reset by PDS0002 |
PDS0006 |
(suppressor) | — | Suppresses CA1815 on a union declaration |
PDS0007 |
(suppressor) | — | Suppresses CA1034 on an extension block |
PDS0008 |
Security | Warning | A BFF's authentication cookie must not be set HttpOnly = false (see Blazor WebAssembly and BFF) |
PDS0009 |
Security | Warning | A BFF's authentication cookie must not be set SameSite = None |
PDS0001, PDS0005, PDS0006 and PDS0007 are DiagnosticSuppressors rather than analyzers, so they
declare SuppressionDescriptors and do not appear in AnalyzerReleases.Shipped.md — that file tracks
analyzer rules only.
A union is a value type, so CA1815 ("override Equals and the equality operators on value types") fires
on every one. The members it asks for cannot sensibly be written: C# does not synthesise value equality for
a union declaration, and a union is read by matching its active case rather than by comparing two unions.
Without the suppression every union declaration would need a #pragma warning disable CA1815 or a [SuppressMessage] beside it.
Both spellings are recognised, because both produce a union and both are reported by CA1815:
// the union keyword
public readonly union TenantError(NotFound, Disabled);
// System.Runtime.CompilerServices.UnionAttribute, which is also how the keyword form is
// encoded in metadata, so a union from a referenced assembly is recognised too
[Union]
public struct AttributedUnion { public int Tag; }The suppression is deliberately narrow. A union's case types are ordinary value types and keep the warning, as does every other struct and any type nested inside a union:
public readonly record struct NotFound(int Id); // CA1815 still reported
public readonly union TenantError(NotFound); // CA1815 suppressedAdd PDS0006 to NoWarn to turn the suppression off, which brings CA1815 back for every union.
A C# 14 extension block is compiled into a nested type inside its owning static class, so CA1034
("nested types should not be visible") fires on every one — reported at the extension keyword and naming
nothing, because the generated type has no source name:
Lib.cs(5,2): error CA1034: Do not nest type . Alternatively, change its accessibility so that it is not
externally visible.
The rule's advice cannot be taken: the nesting is the language's own encoding of the feature, not a design
choice. CA1034 ships at error in the shipped .editorconfig, so without this suppression a single
extension block fails the build, and the only alternative is a #pragma warning disable CA1034 or a
[SuppressMessage] beside every one.
The suppression is deliberately narrow — only the extension block's own declaration is suppressed, and an ordinary nested type keeps the warning whether it sits beside the block or inside it:
public static class StringExtensions
{
extension(string value) // CA1034 suppressed
{
public bool IsBlank => string.IsNullOrWhiteSpace(value);
}
public class Options { } // CA1034 still reported
}Add PDS0007 to NoWarn to turn the suppression off, which restores CA1034 for every extension block.
PDS0004 follows .NET naming guidance for well-known framework spellings (Sql, Uuid, Url, Dns,
Http, Xml, DbContext, ...) while still enforcing uppercase for the acronyms Api → API,
Ai → AI, Ui → UI, Io → IO, Os → OS, Cpu → CPU, Gpu → GPU, Cli → CLI,
Gui → GUI, Ram → RAM, Ssh → SSH and Dsl → DSL. By default the acronym-like segments Http, Xml, Json, Id, Sdk, Sql, Uuid,
Url, Dns, Tcp, Udp, Csv, Pdf, Html, Css, Ftp, Smtp, Imap, and Db are exempt —
Db is deliberately exempt so the prevalent EF Core/ADO.NET spellings (DbContext, DbConnection,
DbSet, CreateDbContext) are never flagged; a repo that prefers DB can re-enable it via
acronym_map = Db:DB. All options are customisable per repo, project, or folder via .editorconfig and
merge with the shipped defaults — config entries override them (so you can opt into Sql → SQL
or opt out of Cli → CLI without re-declaring every default):
dotnet_analyzer_configuration.pds0004.allowed_words— semicolon-separated segments that are never flagged.dotnet_analyzer_configuration.pds0004.acronym_map— semicolon-separatedKey:Valuecorrections.dotnet_analyzer_configuration.pds0004.allowed_identifiers— semicolon-separated whole identifiers that are never flagged. Matched in the order listed (first match wins) by exact name or word-boundary prefix, so a brand name likeCosmosDbalso coversCosmosDbServer/CosmosDbContext.
Members whose names are mandated by a contract — interface implementations (implicit or explicit) and base-class overrides — are never renamed, since doing so would break the contract.
[*.cs]
# Optional overrides; everything not mentioned keeps its shipped default.
dotnet_analyzer_configuration.pds0004.allowed_words = Cli
dotnet_analyzer_configuration.pds0004.acronym_map = Db:DB
dotnet_analyzer_configuration.pds0004.allowed_identifiers = ICosmosDBService;CosmosDb;GitHub;YouTubeThe code fix for PDS0004 renames the identifier and all of its references across the solution.
By default (EnableAssemblyNameGeneration=true), the SDK treats RootNamespace as the canonical public name: AssemblyName and PackageId both default to the fully evaluated RootNamespace — or, when suffix-stripping removed a segment of the logical project name, to the full logical project name so assemblies stay distinct. The defaults are applied during Sdk.props evaluation — before the Microsoft SDK computes TargetName and before the project body — so compilation, output paths, project references, restore, and packing all agree on the same identities. Set EnableAssemblyNameGeneration=false before the SDK import to opt out and fall back to standard .NET behaviour ($(MSBuildProjectName)).
With the default enabled:
| Project name | NamespacePrefix |
RootNamespace |
Resolved AssemblyName / PackageId |
|---|---|---|---|
Api |
Acme |
Acme.Api |
Acme.Api |
Acme.Api |
Acme |
Acme.Api |
Acme.Api (no double-prefix) |
Core.Infrastructure |
Acme |
Acme.Infrastructure |
Acme.Core.Infrastructure (.Core stripped from the namespace only) |
Shared |
Acme |
Acme |
Acme.Shared (full logical name, so it stays distinct) |
ServiceDefaults |
Acme |
Acme |
Acme.ServiceDefaults (full logical name, so it stays distinct) |
Acme |
Acme |
Acme |
Acme |
A <RootNamespace> set before the SDK import replaces the logical project name, and AssemblyName/PackageId follow it instead of re-appending a stripped suffix — but it is still stripped by both passes. Only a <RootNamespace> declared in the project file itself is left exactly as written. The table shows the evaluated RootNamespace; the build strips the wider list as well, so Core.Infrastructure compiles as Acme (see Assembly Name Generation).
Test projects keep their detected suffix: Api.UnitTests → AssemblyName/PackageId = Acme.Api.UnitTests, while RootNamespace remains Acme.Api.
Explicit <AssemblyName> or <PackageId> in a .csproj (or Directory.Build.props) always takes precedence. Because the defaults run before the project body, project-authored values set in the body are evaluated later and win.
Note: set
EnableAssemblyNameGeneration=falsebefore the SDK import (for example inDirectory.Build.props) — it is consumed duringSdk.propsevaluation.
The SDK sets ManagePackageVersionsCentrally=true. The templates/Directory.Packages.props file contains a pinned PackageVersion for every package the SDK auto-adds, grouped under version properties (TUnitVersion, AspireVersion, …). A PackageVersion is a minimum, not an exact pin.
To pin a package:
<PackageVersion Include="TUnit" Version="1.45.29" />To add project-specific packages, just append PackageVersion entries to your Directory.Packages.props.
dotnet build src/BuildSdk.slnx -c Release
dotnet test --solution src/BuildSdk.slnx -c Release
dotnet pack src/src/BuildSdk/BuildSdk.csproj -o ./artifactsMIT