Skip to content
purview-devPublic

About

A reusable MSBuild SDK providing convention-driven .NET project defaults, code style, analyzers, testing frameworks, package management, and agent tooling.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

230 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build SDK

NuGet version Release

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.

What's included

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

Quick start

1. Add the SDK to global.json

{
  "test": {
    "runner": "Microsoft.Testing.Platform"
  },
  "msbuild-sdks": {
    "Purview.BuildSdk": "2.1.0"
  }
}

2. Create Directory.Build.props at repo root

<Project>
  <PropertyGroup>
    <!-- Required: sets the root namespace prefix for all projects -->
    <NamespacePrefix>YourCompany</NamespacePrefix>
  </PropertyGroup>

  <Import Sdk="Purview.BuildSdk" Project="Sdk.props" />
</Project>

3. Create Directory.Build.targets at repo root

<Project>
  <Import Sdk="Purview.BuildSdk" Project="Sdk.targets" />
</Project>

4. Copy Directory.Packages.props to repo root

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=true is set by the SDK. You must have a Directory.Packages.props at your repo root for CPM to work, even if it only contains the packages the SDK adds automatically.

5. Required settings and versions

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.

6. Setting the target framework(s)

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:

  1. Declare it explicitly — TargetFramework for 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 a Condition), in a file the project imports, or in Directory.Build.props before or after the import. Only Directory.Build.targets is too late — the .NET SDK has already decided whether the project multi-targets by then.
  2. Select a curated set — <PurviewTargetFrameworkSet>Supported</PurviewTargetFrameworkSet> in Directory.Build.props or the .csproj. Named sets are Current (the default), Latest, Supported, Broad and All, so a lifecycle change is one SDK bump rather than an edit in every project.
  3. Redefine a set — set PurviewTargetFrameworks<Set> (for example PurviewTargetFrameworksSupported) 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.


Engineering principles

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:

Project naming guide

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.

Defaults (no extra configuration)

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: InternalsVisibleTo follows $(AssemblyName) — so for Api.csproj the SDK generates Acme.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:

  • TUnit
  • TUnit.Mocks
  • Bogus
  • Microsoft.Testing.Platform integration

Specialised additions such as TUnit.Aspire and Testcontainers stay explicit and intentional.

Recommended structure

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.

Quick reference

# 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.UnitTests

For the full layout, naming, identity, and test-readability conventions, see Engineering Principles and Project Naming Conventions.


Template files

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.

.editorconfig

The bootstrapped .editorconfig opens with a version marker:

# Purview.BuildSdk 2.1.0 - managed file, do not edit

That line is how the SDK recognises a file it wrote. When the SDK version changes, a file carrying 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>/.editorconfig carries 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.

Adding your own rules

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 = none

EditorConfig 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.

Turning the update off

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.

Finding the repository root

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.

Agent skills

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 directory

These 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.

Configuration reference

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.

Version detection

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:

  1. Explicit path — if RootPackageJson is set, reads that file directly.
  2. Auto-discovery — otherwise, finds the repository root (RepoRoot) and reads package.json from there: a RepoRoot set 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 .git and then the nearest package.json above 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, EnableVersionDetectionCache and VersionDetectionCacheFile must be set before the <Import Sdk="Purview.BuildSdk" Project="Sdk.props" /> line in your Directory.Build.props. The version logic runs during that import and cannot see properties set afterwards (e.g. in individual .csproj files).

<Project>
  <PropertyGroup>
    <NamespacePrefix>Acme</NamespacePrefix>
    <!-- Set here, before the import -->
    <RootPackageJson>$(MSBuildThisFileDirectory)package.json</RootPackageJson>
  </PropertyGroup>

  <Import Sdk="Purview.BuildSdk" Project="Sdk.props" />
</Project>

General

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.

Packable project defaults

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.

Repo bootstrap

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.

Sdk folder packaging

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.

Agent skills

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/

Telemetry

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.

Testing

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.

Target framework sets

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.

Project detection

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.

Blazor WebAssembly and browser-safe libraries

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.

Roslyn components and analyzers

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.

Property schema validation

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, ...).

Compiler-visible SDK properties

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.

Example: switch a repo to Xunit + NSubstitute and disable Bogus

<Project>
  <PropertyGroup>
    <NamespacePrefix>Acme</NamespacePrefix>
    <TestingFramework>Xunit</TestingFramework>
    <SubstituteFramework>NSubstitute</SubstituteFramework>
    <TestDataFramework>None</TestDataFramework>
  </PropertyGroup>

  <Import Sdk="Purview.BuildSdk" Project="Sdk.props" />
</Project>

MSBuild property schema and IntelliSense

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 features

.NET 11 is the baseline this SDK targets. The switches below are applied only to a net11.0 (or later) target, so the net10.0 leg of the default net10.0;net11.0 set is unaffected.

Feature Applied Why
Runtime Async V2 (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.

Evaluated and deliberately not defaulted

These improve a specific workload but would change behaviour or cost for every project, so they stay opt-in:

  • Server GC / TieredPGO / ReadyToRun — the runtime and SDK already choose sensible defaults per app type; a blanket ServerGarbageCollection=true is wrong for desktop apps and tests.
  • Native AOT and PublishAot — a publish property, not a build default, and it changes the artifact and the libraries an app may use.
  • UseArtifactsOutput — a much faster, cache-friendly layout, but a breaking change to every output path a repository may reference.
  • EnablePackageValidation — needs a baseline package, so it is a per-package decision.
  • NuGetAuditMode=all — a good security default, but it turns transitive vulnerability warnings on for every repository, which is a policy decision rather than a build default.
  • CheckForOverflowUnderflow — improves correctness at a measurable runtime cost; opt in where arithmetic overflow is a real risk.

The SDK does adopt the .NET 11 behaviour that needs no switch: AnalysisLevel=latest now resolves to the .NET 11 rule set, the MSBuild server is on by default in the SDK, and the default C# version is latest (C# 15).


SourceLink

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

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 a ServerShared project.
  • PRSHR0002 — a Shared project references a ServerShared project.

Both are warnings; PurviewServerSharedReferenceCheckAsError=true makes them errors and DisablePurviewServerSharedReferenceCheck=true suppresses them. PurviewBrowserSafe="true" on the ProjectReference is accepted as the explicit statement that the reference was judged safe, the same escape hatch PRBLZ0001 uses. PurviewAutoServerSharedReference=false removes the automatic reference entirely.


Blazor WebAssembly and BFF

Every behaviour here applies only to the project type it names. A project that is not a WebAssembly client, a Blazor host, a BFF or a PurviewBrowserSafe library is unchanged.

Detection

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.

WebAssembly client defaults

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.

Browser-safe libraries

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 on IsTrimmable and the trim, single-file and AOT analyzers;
  • <SupportedPlatform Include="browser" />, so CA1416 reports browser-unsupported APIs such as System.Diagnostics.Process;
  • WarningsAsErrors for each trim, single-file, AOT and CA1416 id (PurviewBrowserSafeWarningCodes), rather than TreatWarningsAsErrors, so the rest of the project's warning policy is untouched. PurviewBrowserSafeWarningsAsErrors=false opts out, handing the ids back to TreatWarningsAsErrors (true by default); CA1416 then falls back to the repository's severity (suggestion in 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.

BFF hosts

  • 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 setting PurviewBffExposesApi=true, keeps it.
  • PDS0008 reports HttpOnly = false and PDS0009 reports SameSite = None on the CookieAuthenticationOptions cookie - 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 project naming conventions

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.

Shared testing projects

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.


InternalsVisibleTo

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 use Custom.Assembly.UnitTests, Custom.Assembly.IntegrationTests, etc.
  • Default — AssemblyName is RootNamespace-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:

  1. TestType variants — for each defined TestType (Unit, Integration, Architecture, Contract, Functional, …) the SDK emits the $(AssemblyName).{TestType}Tests alias plus the $(TargetProjectName).{TestType}Tests and $(MSBuildProjectName).{TestType}Tests equivalents — 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 explicit AssemblyName, the SDK default, or the raw project name.
  2. SharedTesting projects — one per known shared testing project name (SharedTestingFramework, SharedTestingInfrastructure, etc.). By default (EnableAssemblyNameGeneration=true) with a NamespacePrefix set, these are prefixed (e.g. Acme.SharedTestingFramework); with EnableAssemblyNameGeneration=false the raw name is used.

Disabling automatic InternalsVisibleTo

To disable automatic InternalsVisibleTo generation, set DisableAutoInternalsVisibleTo=true in your project or Directory.Build.props:

<PropertyGroup>
  <DisableAutoInternalsVisibleTo>true</DisableAutoInternalsVisibleTo>
</PropertyGroup>

EmbeddedAttribute generation

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_EMBEDDED

AssemblyInfo 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.


Assembly identifier class

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.


Namespace stripping

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 produced IDE0130 across every Roslyn-component repository consuming this SDK.
  • Persistence, AppHost and Data — they read as layer noise but are routinely the whole point of the project.
  • Anything a project opts back in with <NamespaceRemoveSuffix Include="..." />.

Keeping suffixes (opt a stripped suffix back in)

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.

Extensions namespace rule (PDS0002)

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)
  • RootNamespace is 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.


SDK-shipped analyzers

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.

Style policy

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 (private inside a type, internal at namespace scope, public inside an interface) is reported by IDE0040 and must be removed. Omit the default modifier instead.
  • Private instance fields must be _camelCase, never camelCase or PascalCase: the _ prefix keeps field access unambiguous, so the noisy this. qualifier is never needed. Private constants and private static readonly fields are type-level state and stay PascalCase; non-private constants and static readonly fields 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 through InternalsVisibleTo, and CA1034 is no longer disabled for Extensions/ 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 use Method_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, the IDE1006 naming rules). Set DisablePurviewTestContextRuleSet=true to enforce the production rules in tests as well, or override PurviewTestContextNoWarn with 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 CA1515 does 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.

Union equality (PDS0006)

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 suppressed

Add PDS0006 to NoWarn to turn the suppression off, which brings CA1815 back for every union.

Extension block nesting (PDS0007)

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-separated Key:Value corrections.
  • 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 like CosmosDb also covers CosmosDbServer/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;YouTube

The code fix for PDS0004 renames the identifier and all of its references across the solution.


Assembly name generation

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=false before the SDK import (for example in Directory.Build.props) — it is consumed during Sdk.props evaluation.


Central Package Management

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.


Documentation

Building the SDK

dotnet build src/BuildSdk.slnx -c Release
dotnet test --solution src/BuildSdk.slnx -c Release
dotnet pack src/src/BuildSdk/BuildSdk.csproj -o ./artifacts

License

MIT

About

A reusable MSBuild SDK providing convention-driven .NET project defaults, code style, analyzers, testing frameworks, package management, and agent tooling.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages