# Build SDK > Convention-driven MSBuild SDK for .NET project defaults. 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. - Repository: https://github.com/purview-dev/build-sdk - Package: https://www.nuget.org/packages/Purview.BuildSdk - Project page: https://purview.dev/projects/build-sdk/ - Documentation: https://purview.dev/docs/build-sdk/ - Full machine-readable content: https://purview.dev/projects/build-sdk/llms-full.txt # Getting Started This guide walks through the minimal setup required to adopt `Purview.BuildSdk` in a repository. Install it once per repo — every project beneath the repo root inherits everything automatically. ## 1. Add the SDK to `global.json` Add `Purview.BuildSdk` to the `msbuild-sdks` section so MSBuild can resolve the SDK: ```json { "test": { "runner": "Microsoft.Testing.Platform" }, "msbuild-sdks": { "Purview.BuildSdk": "1.0.0" } } ``` :::note The SDK can also bootstrap a `global.json` for you at the repository root when one is missing; see [Repository Bootstrap](repository-bootstrap/). ::: ## 2. Create `Directory.Build.props` at the repo root ```xml YourCompany ``` `NamespacePrefix` is mandatory — a build error is raised if it is missing (`ValidateRootNamespacePrefixTarget`), unless `DisableNamespacePrefixCheck=true`. ## 3. Create `Directory.Build.targets` at the repo root ```xml ``` ## 4. Copy `Directory.Packages.props` to the repo root Copy `templates/Directory.Packages.props` from this package to your repo root. All package versions default to `*` (latest at restore). Pin any package by replacing `*` with a specific version. > **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. ## Next steps - Read the policy-level guidance in [Engineering Principles](engineering-principles/). - Understand how projects are detected and named in [Project Type Detection](project-type-detection/) and [Project Naming Conventions](project-naming-conventions/). - Browse every configurable property in the [Configuration Reference](configuration-reference/). - Learn how the package version is sourced in [Version Detection](version-detection/). - See how test projects are wired up in [Testing Wiring](testing-wiring/). - Read how packages are produced in [Packaging](packaging/). --- # Engineering Principles `Purview.BuildSdk` treats naming, placement, and test structure as configuration. The SDK can only infer the right namespaces, identities, references, and test wiring when projects follow a small, predictable set of conventions. This page is the policy-level guide for those conventions. Use it when creating a new repository, adding a new project, or rationalising an older repository toward the SDK defaults. The companion pages explain the mechanics in detail: - [Project Naming Conventions](../project-naming-conventions/) - [Project Type Detection](../project-type-detection/) - [Assembly Name Generation](../assembly-name-generation/) - [Testing Wiring](../testing-wiring/) ## Core principles - Naming is part of configuration. - Folder placement is part of configuration. - `NamespacePrefix` is the root identity source for the repository. - `RootNamespace` is the canonical code identity by default. - `AssemblyName` and `PackageId` should normally align with the resolved project identity. - Test suites should optimise readability at scale, not just local convenience. - Automatic SDK behavior should be preferred over manual per-project overrides. These conventions exist to make repository structure legible to both humans and tools. A repository with hundreds or thousands of tests becomes easier to navigate when project names, namespaces, categories, and test class names all tell the same story. ## Canonical project layout The SDK is intentionally tolerant of both `src/` + `tests/` and flat layouts, but new repositories should prefer an explicit source/test split. Recommended layout: ```text MyRepo/ |- Directory.Build.props |- Directory.Build.targets |- Directory.Packages.props |- global.json |- package.json |- src/ | |- MyProduct.slnx | |- src/ | | |- Domain/ | | | '- Domain.csproj | | |- Identity.API/ | | | '- Identity.API.csproj | | '- Web.App/ | | '- Web.App.csproj | '- tests/ | |- Domain.UnitTests/ | | '- Domain.UnitTests.csproj | |- Identity.API.IntegrationTests/ | | '- Identity.API.IntegrationTests.csproj | '- SharedTestingInfra/ | '- SharedTestingInfra.csproj '- README.md ``` Canonical patterns: - solution entry point: `src/{SolutionName}.slnx` - source projects: `src/src/{ProjectName}/{ProjectName}.csproj` - test projects: `src/tests/{ProjectName}.{TestType}Tests/{ProjectName}.{TestType}Tests.csproj` The `.csproj` filename should match the containing directory name unless the repository explicitly opts out with `DisableProjectFileNamingConventionCheck=true`. ## Project naming rules Use short project names. The SDK applies the prefixing and identity generation for you. Good examples: - `Hosting.csproj` - `Identity.API.csproj` - `Domain.UnitTests.csproj` - `SharedTestingInfra.csproj` Avoid redundant prefixing: - `Aspire.Hosting.csproj` - `Acme.Sales.RegionalPipeline.Identity.API.csproj` The SDK derives behavior from project names, so near-miss names are a liability. If you want the SDK's automatic shared/shared-testing behavior, use its exact recognised names. Recognised shared project names: - `Shared` - `SharedFramework` - `SharedInfrastructure` - `SharedInfra` - `SharedUtilities` - `SharedUtils` - `SharedLibrary` - `SharedLib` - `SharedHelpers` Recognised shared testing project names: - `SharedTestingFramework` - `SharedTestingInfrastructure` - `SharedTestingInfra` - `SharedTestingUtilities` - `SharedTestingUtils` - `SharedTestingLibrary` - `SharedTestingLib` - `SharedTestingHelpers` ## Namespace, assembly, and package identity `NamespacePrefix` is the source of truth for repository identity. The SDK derives a logical project identity from: - `NamespacePrefix` - the project name - a small set of suffix-stripping rules Key rules: 1. `RootNamespace` defaults to the logical project identity. 2. Duplicate tail segments are collapsed, so an already-prefixed project does not get double-prefixed. 3. Test suffixes such as `.UnitTests` and `.IntegrationTests` are removed from `RootNamespace`. 4. Known non-identity suffixes such as `Core`, `EF`, `Shared`, `ClientShared`, and `ServiceDefaults` may be stripped from `RootNamespace`. 5. `AssemblyName` and `PackageId` normally default to the resolved project identity. 6. When suffix stripping would otherwise collapse two distinct artifacts into the same identity, `AssemblyName` and `PackageId` keep the fuller logical identity so the artifacts remain distinct. Examples: | `NamespacePrefix` | Project name | `RootNamespace` | `AssemblyName` / `PackageId` | | -- | -- | -- | -- | | `Aspire` | `Hosting` | `Aspire.Hosting` | `Aspire.Hosting` | | `Aspire` | `Hosting.UnitTests` | `Aspire.Hosting` | `Aspire.Hosting.UnitTests` | | `Acme.Sales.RegionalPipeline` | `Domain` | `Acme.Sales.RegionalPipeline.Domain` | `Acme.Sales.RegionalPipeline.Domain` | | `Acme.Sales.RegionalPipeline` | `Identity.API` | `Acme.Sales.RegionalPipeline.Identity.API` | `Acme.Sales.RegionalPipeline.Identity.API` | | `Acme.Sales.RegionalPipeline` | `Identity.Core` | `Acme.Sales.RegionalPipeline.Identity` | `Acme.Sales.RegionalPipeline.Identity.Core` | | `Acme.Sales.RegionalPipeline` | `Shared` | `Acme.Sales.RegionalPipeline` | `Acme.Sales.RegionalPipeline.Shared` | This distinction matters when reasoning about packaging, `InternalsVisibleTo`, and test assembly names. ## Automatic project references The SDK infers project references from naming and placement. For test projects: - `TargetProjectName` is inferred from the project name. - the target project is auto-discovered from conventional relative paths - sibling shared-testing projects are also discovered automatically For non-test projects: - sibling shared projects are discovered automatically This means naming discipline is not cosmetic. It directly affects whether the SDK can connect the right projects without manual `ProjectReference` maintenance. ## Test project types and categories The detected test type becomes the baseline category for the project. Examples: - `*.UnitTests` -> `Category=Unit` - `*.IntegrationTests` -> `Category=Integration` - `*.ContractTests` -> `Category=Contract` The detected test category is the default category, not the only one. Additional categories are allowed and encouraged when they improve filtering and discoverability in large suites. Recommended common test project types: - `UnitTests` - `IntegrationTests` - `E2ETests` - `FunctionalTests` - `ContractTests` The SDK also recognises broader suffixes when a repository genuinely needs them, including: - `AcceptanceTests` - `PerformanceTests` - `LoadTests` - `SmokeTests` - `StressTests` - `RegressionTests` - `SecurityTests` - `ScenarioTests` - `SystemTests` - `ArchitectureTests` - `AccessibilityTests` - `InteractiveTests` - `EnvironmentTests` - `WhiteBoxTests` - `BlackBoxTests` - `ChaosTests` - `ThreatTests` Prefer the smaller common set by default. Reach for the broader set only when the test type itself carries important operational meaning. ## Default and specialised test wiring Standard test projects automatically receive the default test stack from the SDK: - `TUnit` - `TUnit.Mocks` - `Bogus` - Microsoft.Testing.Platform integration Shared testing projects receive test-support wiring rather than a runnable test host. Specialised dependencies stay explicit and intentional: - `TUnit.Aspire` for Aspire lifecycle or AppHost-backed integration tests - `Testcontainers` for container-backed integration tests Do not manually duplicate the default stack in every test project unless the repository has explicitly opted out of the SDK defaults. ## Test readability rules The goal of the naming conventions is readability when a repository contains thousands of tests. ### Subject-based tests When a test class owns a specific subject, prefer `{SubjectName}Tests`. Examples: - `CustomerIdTests` - `ResultsEndpointFilterTests` - `AssemblyNameCalculatorTests` When a test method targets a specific member or subject behavior, use: `{SubjectOrMemberUnderTest}_{Scenario}_{Expectation}` Examples: - `Create_GivenInvalidEmail_ThrowsArgumentException` - `Constructor_GivenNullLogger_ThrowsArgumentNullException` - `DisplayName_WhenTrimmed_ReturnsNormalizedValue` - `CompareTo_GivenHigherVersion_ReturnsPositiveValue` This format is for subject-based tests: methods, constructors, properties, operators, conversions, validation hooks, and similarly well-bounded behavior. ### Non-subject-based suites Not every useful suite is centered on one subject. Broader suite names are allowed when they are more readable and truthful. Examples: - `BuildIntegrationTests` - `GeneratedPackageAssetsTests` - `CrossPlatformSchemaCompatibilityTests` - `AppHostLifecycleTests` Use TUnit features such as display names, categories, and data-driven metadata to keep these broader suites discoverable and understandable. ### Async, assertions, and cancellation - Test methods should be `public async Task`. - Assertions should use TUnit `Assert`. - When the API under test accepts a `CancellationToken`, the test method should accept `CancellationToken cancellationToken` as its final parameter. - Parameters before the token may come from TUnit data sources. - The cancellation token should be forwarded to the API under test and to helper methods that also accept one. ## Rationalising existing repositories Older repositories may not fully match these conventions yet. Apply them pragmatically: - new projects should follow these rules by default - existing projects should converge over time - preserve readability and avoid churn-only renames - when in doubt, prefer the structure that lets the SDK infer behavior without extra overrides Use these engineering principles as the policy layer, then use the companion pages for the concrete mechanics of detection, naming, identity generation, and test wiring. --- # Project Naming Conventions This page explains the mechanics behind the higher-level guidance in [Engineering Principles](../engineering-principles/). Read that page first when you need the policy and tradeoffs; use this page when you need the exact naming and layout rules the SDK implements. The SDK applies several conventions automatically based on the `.csproj` filename and `NamespacePrefix`. ## Defaults (no extra configuration) `RootNamespace` is always derived from `$(NamespacePrefix).$(ProjectName)` and is the canonical default public name. By default (`EnableAssemblyNameGeneration=true`), `AssemblyName` and `PackageId` both follow the fully evaluated `RootNamespace`. Test projects retain 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 behaviour (the `.csproj` filename): | `.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: ```text ✅ 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. ## Recommended structure: `src/` + `tests/` For larger repos, separate source and test projects into `src/` and `tests/` folders: ```text MyRepo/ ├── Directory.Build.props ← NamespacePrefix=Acme ├── Directory.Build.targets ├── Directory.Packages.props ├── global.json ├── src/ │ ├── Api/ │ │ └── Api.csproj │ ├── Core/ │ │ └── Core.csproj │ └── SourceGenerator/ │ └── SourceGenerator.csproj ├── tests/ │ ├── Api.UnitTests/ │ │ └── Api.UnitTests.csproj → IsTestProject=true, TestingType=Unit │ ├── Api.IntegrationTests/ │ │ └── Api.IntegrationTests.csproj │ └── SharedTestingFramework/ │ └── SharedTestingFramework.csproj → IsSharedTestingProject=true └── package.json ``` ## Flat structure: everything together For smaller repos, source and test projects can live side-by-side: ```text MyRepo/ ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.Packages.props ├── global.json ├── Api/ │ └── Api.csproj ├── Api.UnitTests/ │ └── Api.UnitTests.csproj ├── Core/ │ └── Core.csproj ├── Core.IntegrationTests/ │ └── Core.IntegrationTests.csproj └── package.json ``` Both layouts work identically — the SDK detects test projects by name suffix, not folder location. ## Quick reference ```sh # Create a source project mkdir src/Api && cd src/Api dotnet new classlib -n Api # Create its unit tests mkdir ../../tests/Api.UnitTests && cd ../../tests/Api.UnitTests dotnet new classlib -n Api.UnitTests # SDK wires TUnit automatically # Or flat: mkdir Api.UnitTests && cd Api.UnitTests dotnet new classlib -n Api.UnitTests ``` ## Test project naming conventions Test projects are automatically detected by their suffix. The common recommended set is: ```text MyProject.UnitTests → IsTestProject=true, TestingType=Unit MyProject.IntegrationTests→ IsTestProject=true, TestingType=Integration MyProject.E2ETests → IsTestProject=true, TestingType=E2E MyProject.FunctionalTests → IsTestProject=true, TestingType=Functional MyProject.ContractTests → IsTestProject=true, TestingType=Contract ``` Any suffix from the full list is recognised: `Unit`, `Integration`, `E2E`, `EndToEnd`, `Acceptance`, `Functional`, `Performance`, `Load`, `Smoke`, `Stress`, `Regression`, `Security`, `Chaos`, `Scenario`, `System`, `Threat`, `BlackBox`, `WhiteBox`, `Accessibility`, `Interactive`, `Environment`, `Architecture`, `Contract`. The detected test type becomes the baseline category for the project. Additional categories can still be added when they improve discoverability or filtering. ## 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. See [Project Type Detection](../project-type-detection/) for how these names are classified, and [Assembly Name Generation](../assembly-name-generation/) for how the identities are derived. --- # Project Type Detection This page documents the detection mechanics behind the policy described in [Engineering Principles](../engineering-principles/). During `Sdk.props` evaluation the SDK classifies every `.csproj` by reading the project filename, the `Sdk` attribute, and on-disk markers. The resulting flags drive the defaults described throughout this wiki. ## Detection flags | Flag | Condition | | -- | -- | | `IsCSharpProject` | Project file extension is `.csproj`. | | `IsTestProject` | Project name ends with `Test`/`Tests` and carries a recognised `TestingType` suffix. | | `IsSharedTestingProject` | Project name is one of `SharedTestingFramework`, `SharedTestingInfrastructure`, `SharedTestingInfra`, `SharedTestingUtilities`, `SharedTestingUtils`, `SharedTestingLibrary`, `SharedTestingLib`, `SharedTestingHelpers`. | | `IsSharedProject` | Project name is one of `Shared`, `SharedFramework`, `SharedInfrastructure`, `SharedInfra`, `SharedUtilities`, `SharedUtils`, `SharedLibrary`, `SharedLib`, `SharedHelpers`. | | `IsContainerProject` | A `Dockerfile`, `dockerfile`, or `Dockerfile.dev` exists in the project directory. | | `IsSdkProject` | An `Sdk` value is parsed from the ``/`` element. | | `IsWebSdkProject` | `SdkProjectName` is `Microsoft.NET.Sdk.Web`. | | `IsWorkerSdkProject` | `SdkProjectName` is `Microsoft.NET.Sdk.Worker`. | | `IsAspireHostProject` | `SdkProjectName` starts with `Aspire.Sdk.Host` or `Aspire.AppHost.Sdk`. | | `IsCLIProject` | Project name ends with `CLI`, `Console`, `CommandLine`, `QuickStart`, or `QuickStarts`. | | `IsRoslynComponent` | `true` is declared in the project file. | | `IsPackable` | `true` is declared in the project file. | | `IsWebProject` | Marker used in SDK web-project behaviour. | `TestingType` is the detected test category suffix from the project name (for example `Unit`, `Integration`, `E2E`); `TargetProjectName` is the inferred non-test project name that a test project targets. The detected `TestingType` also becomes the baseline test category, while additional categories remain available to the test suite. The SDK recognises the following full `TestType` set, in addition to the standard `Unit` and `Integration` values: `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`, and `WhiteBox`. ## What each type gets ### Test projects (`IsTestProject=true`) - `OutputType=Exe` when the testing framework is not `None`. - A **test-context rule set** (`PurviewTestContextNoWarn`): the production API-surface rules (`CA1002`, `CA1012`, `CA1034`, `CA1047`, `CA1050`, `CA1051`, `CA1062`, `CA1064`, `CA1515`, `CA1707`) are exempt, while the strict style contract (`IDE0040`, field naming, formatting, `IDE1006` naming rules) still applies. `DisablePurviewTestContextRuleSet=true` enforces the production rules instead. - `CollectCoverage=true` with coverage exclusions for framework and mocking packages. - `IsPackable=false`, `IsPublishable=false`, `MaxCpuCount=0`. - Disabled native instrumentation by default. - An `[assembly: ExcludeFromCodeCoverage]` attribute. - Automatic `ProjectReference` to the target project (resolved as `..//`, sibling `src/` paths, and the shared testing project alongside). - A `[Category: ]` assembly attribute (TUnit) or `[Trait("Category", ...)]` (Xunit). ### Shared testing projects (`IsSharedTestingProject=true`) - Test package references, but not the test runner or coverage settings. - A `[Skip]` attribute (TUnit) so the shared assembly is never executed directly. - `TUnit.Core` instead of the full `TUnit` package. - `OutputType=Library` with `IsTestProject`/`IsTestingPlatformApplication` cleared, reasserted after the package props (which otherwise flip the project into an executable test host). The fixtures are consumed by the test assemblies, so staying a library avoids `CA1515` (which only targets executables). Set `PurviewSharedTestingOutputType=Exe` before the SDK import to keep the package-driven test-host shape. ### Shared projects (`IsSharedProject=true`) - Automatically referenced by sibling non-test projects via wildcard (`../Shared*/Shared*.csproj`); test projects automatically reference `../SharedTesting*/SharedTesting*.csproj`. ### Container projects - `InvariantGlobalization=true`, `PublishAot=true`, `DockerDefaultTargetOS=Linux`, `DockerfileContext=..\..\`. - Adds `Microsoft.VisualStudio.Azure.Containers.Tools.Targets`. ### Web SDK projects (`IsWebSdkProject=true`) - Non-API web apps get `InterceptorsNamespaces` extended with `Microsoft.AspNetCore.OpenApi.Generated` for OpenAPI interceptors. ### CLI projects - `OutputType=Exe`, `appsettings*.json` copied to the output directory. - `CA1515` suppressed (nested settings classes on internal commands). ### Aspire host projects - `OutputType=Exe` (when not a test/shared-testing project), `CA1515` suppressed. ### Roslyn components (`IsRoslynComponent=true`) - Default `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `EnforceExtendedAnalyzerRules=true`, `Deterministic=true`. - Compiler-generated output under the intermediate directory, no dependency file. - No symbol package by default; the PDB ships beside the analyzer in the package. - SourceLink with `EmbedUntrackedSources=true`, telemetry excluded. - See [Packaging](../packaging/) for how the analyzer assets are packed. ### Packable projects (`IsPackable=true`) - `GenerateDocumentationFile`, `IncludeSource`, `IncludeSymbols` (`.snupkg`), `PublishRepositoryUrl`, `EmbedUntrackedSources`, `DebugType=portable` defaults. - See [Packaging](../packaging/). ## Properties exposed to the compiler The detection flags (plus many SDK properties) are exposed to Roslyn analyzers and source generators via `CompilerVisibleProperty`, readable as `build_property.`. The full list is in the [Configuration Reference](../configuration-reference/). --- # Configuration Reference Set any of these properties **before** the `` in your `Directory.Build.props` (most are consumed during `Sdk.props` evaluation). Explicit values set before the import are always preserved. ## 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`. Relative paths are resolved from the project directory. | | `EnableVersionDetectionCache` | `true` | Enables local caching of auto-discovered package.json version results. | | `VersionDetectionLogEnabled` | `false` | Emits a high-importance message showing the detected package version. | See [Version Detection](../version-detection/) for the full resolution rules. ## 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 `ValidatePurviewStylePolicy` failing the build (`PRSGD0006`-`PRSGD0009`) when the repository `.editorconfig` overrides `dotnet_style_require_accessibility_modifiers`, hides `IDE0040`/`IDE1006`, disables the Style category in bulk, weakens the `_camelCase` private instance field rule, or adds the accessibility rules to `NoWarn`. Entries the SDK injects itself (the test-context rule set, `CA1515` for Aspire hosts and CLI apps) are ignored. See [Style policy](../analyzers/#style-policy). | | `PurviewTestContextNoWarn` | `CA1002;CA1012;CA1034;CA1047;CA1050;CA1051;CA1062;CA1064;CA1515;CA1707` | Production API-surface rules exempted in test and shared-testing projects (they keep the strict style contract). Override before the SDK import to narrow or extend the list. | | `DisablePurviewTestContextRuleSet` | `false` | Set to `true` to make test and shared-testing projects enforce the production API-surface rules as well. | | `PurviewSharedTestingOutputType` | `Library` | Output type forced on `IsSharedTestingProject` projects; `Library` also clears `IsTestProject`/`IsTestingPlatformApplication`. Set to `Exe` before the SDK import to keep the test packages' executable/test-host shape. | | `TargetFramework` | `net10.0` | Override the default TFM per-project or globally. Defaults to `netstandard2.0` for projects declaring `IsRoslynComponent=true`. | | `IsRoslynComponent` | `false` | When explicitly `true`, applies source-generator defaults: a single `netstandard2.0` target, `LangVersion=latest`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `Deterministic=true`, extended analyzer rules, SourceLink with `EmbedUntrackedSources=true`, compiler-generated output under the intermediate directory, no dependency file, symbol packaging (`IncludeSymbols=false` by default), telemetry exclusion, and package build output. Packable Roslyn components automatically pack the built analyzer assembly and its PDB into `analyzers/dotnet/cs/` (`PurviewPackAnalyzerPdb=true`; set `false` only when symbols are delivered another way — NuGet's `.snupkg` cannot host `analyzers/dotnet/cs` symbols). Pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack if the compiler defaults are missing unless `DisableRoslynCompilerDefaultsValidation=true`. Roslyn development dependencies (`Microsoft.CodeAnalysis.*`, `Microsoft.CodeAnalysis.Analyzers`) default to `PrivateAssets="all"`. | | `PackProjectReferencedSourceGenerators` | `true` | Automatically packs analyzer `ProjectReference` outputs and their runtime dependencies under `analyzers/dotnet/cs/`. Set to `false` to opt out; set `Pack="false"` on an individual reference to exclude only that generator. | | `SourceLinkPackageName` | `Microsoft.SourceLink.GitHub` | SourceLink provider. Set to `Microsoft.SourceLink.AzureDevOps.Git` for ADO repos. | | `DisableSourceLink` | `false` | Set to `true` to stop the SDK from adding the configured SourceLink package automatically. | | `EnableAssemblyNameGeneration` | `true` | When `true` (default), `AssemblyName` and `PackageId` derive from the fully evaluated `RootNamespace`. When explicitly `false`, standard .NET behaviour applies (`$(MSBuildProjectName)`). Explicit ``/`` in a `.csproj` always take precedence. | | `DisableProjectFileNamingConventionCheck` | `false` | Set to `true` to disable the validation that requires `MyProject\MyProject.csproj` naming alignment. | | `DisableGenerateAssemblyInfoClass` | `false` | Set to `true` to disable the generated `AssemblyInfo` helper source. | | `AutoIncludeUsings` | `true` | Controls SDK-added global usings for `NamespacePrefix` and `RootNamespace`. | | `NamespaceRemoveSuffix` | *(built-in list)* | Item type listing the suffixes stripped from `RootNamespace`. Remove an entry **after** the `Sdk.props` import to keep that suffix in the namespace, e.g. ``. See [Namespace stripping](../assembly-name-generation/#namespace-stripping). | ## Repository metadata The SDK also supports repo/site metadata properties for generated package metadata and docs links. These values are consumed before the import and are useful when a repository wants a single shared project URL or docs base URL instead of repeating it per project: | Property | Default | Description | | -- | -- | -- | | `PurviewHomepage` | *(repo-specific)* | Base site URL for the repository or product docs. In this repo it is set to `https://purview.dev/`. | | `PurviewProjectUrl` | *(repo-specific)* | Public project page URL; usually derived from `PurviewHomepage` plus a project path. | | `PurviewDocsUrl` | *(repo-specific)* | Public docs URL; often derived from `PurviewHomepage` plus a docs path. | These values are not mandatory, but they provide a consistent place to keep package metadata and docs links aligned with the repo's site structure. ## Packable project defaults For projects where `IsPackable=true`, the SDK provides these defaults **only when the consuming project has not supplied a value**: | Property | Default | Description | | -- | -- | -- | | `GenerateDocumentationFile` | `true` | Emits XML documentation. | | `IncludeSymbols` | `true` | Produces a symbol package (`false` for Roslyn components — 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` | `true` | Includes source files in the package. | 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`). 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 default `WarnOnPackingNonPackableProject=false`, so solution-wide pack operations skip them silently. ## 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`. | | `PurviewBuildSdkVersionForGlobalJson` | *(auto-detected or `1.0.0` fallback)* | Version written to the `msbuild-sdks.Purview.BuildSdk` entry in a bootstrapped `global.json`. | | `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing` never touches an existing file, `Always` overwrites it, `WarnOnDrift` reports that an existing file differs from the SDK-provided one, and `Never` skips bootstrapping entirely. | | `PurviewRepoBootstrapCopyRetries` | `3` | Copy attempts before a bootstrap write failure is reported. | | `PurviewRepoBootstrapCopyRetryDelayMilliseconds` | `500` | Base delay between bootstrap write attempts. | | `PurviewRepoBootstrapCopyFailureAsError` | `true` | When `false`, a bootstrap write that still fails after every retry is reported as a warning and the build continues. | | `PurviewSuppressCopyRetryWarnings` | `true` | Demotes MSB3026 copy retry notices to messages. Set to `false` to see every retry attempt. | Bootstrap writes are staged into a temporary file and renamed into place, so editors and tools never observe a partially written `.editorconfig` or `global.json`. Because every project in a solution runs the same bootstrapping targets, a lost race between parallel projects is a no-op: an existing file is treated as success rather than a copy failure. ## Agent folder | 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. | | `EnableAgentFolderInPackage` | `true` | Mirrors the bundled `.agents/**` folder from the SDK NuGet package into the consuming repository's `.agents/` folder (or `$(AgentPackDestinationFolder)/`) before build. | | `AgentPackDestinationFolder` | `.agents` | Repo-relative destination folder that receives the mirrored agent folder contents when `EnableAgentFolderInPackage` is `true`. | | `PurviewAgentFolderSourcePath` | *(package-level `.agents`)* | Overrides the folder that provides the bundled `.agents` content. | | `PurviewAgentFolderCopyRetries` | `3` | Copy attempts per file before the failure is reported. | | `PurviewAgentFolderCopyRetryDelayMilliseconds` | `500` | Base delay between copy attempts. | | `PurviewAgentFolderCopyFailureAsError` | `true` | When `false`, a copy that still fails after every retry is reported as a warning and the build continues. | | `PurviewAgentSyncManifestPath` | `/.purview/agent-sync.cache` | Overrides the change-detection manifest used to skip unchanged agent content. | The mirror is change-aware: content that already matches the manifest is skipped entirely, which keeps repeat builds free of file writes and file locks. Retry notices are demoted to low-importance messages (`MSB3026`); a copy that still fails after every retry is reported as an error that names the source, destination and OS error. To disable bundled agent folder copying in a consuming repo, set the opt-out property before importing the SDK: ```xml false ``` ## 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. | See [Engineering Principles](../engineering-principles/) for the policy-level testing guidance and [Testing Wiring](../testing-wiring/) for the implementation details. ## Compiler-visible SDK properties The SDK exports its properties via `CompilerVisibleProperty`, so analyzers and source generators can read them through `build_property.`: | 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. | | `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`. | | `EnableAgentFolderInPackage` | When `true`, copies the bundled `.agents` folder into the consuming repository. | | `AgentPackDestinationFolder` | Repo-relative destination folder that receives copied `.agents` content. | | `PurviewAutoSdkPack` | When `true`, automatically packs the `Sdk/` folder contents into the NuGet package. | | `DisableGenerateAssemblyInfoClass` | Disables generated `AssemblyInfo` helper source. | | `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; the project is forced to `OutputType=Library` (see `PurviewSharedTestingOutputType`). | | `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`. | | `IsCLIProject` | True when the project is a CLI project. | | `IsSharedProject` | True when the project is a shared project. | | `EditorConfigFilePath` | Path to the SDK-provided `.editorconfig` injected into `@(EditorConfigFiles)`. | | `RepositoryEditorConfigFilePath` | Destination path for bootstrapping a physical repo-level `.editorconfig`. | | `BootstrapEditorConfigToRepoRoot` | When `true` (default), copies the SDK `.editorconfig` to `RepositoryEditorConfigFilePath` if missing. | | `RepositoryGlobalJsonFilePath` | Destination path for bootstrapping a physical repo-level `global.json`. | | `BootstrapGlobalJsonToRepoRoot` | When `true` (default), creates `global.json` at `RepositoryGlobalJsonFilePath` if missing. | | `PurviewBuildSdkVersionForGlobalJson` | Version used for `msbuild-sdks.Purview.BuildSdk` when bootstrapping `global.json`. | | `DisableAutoCopySdkFiles` | When `true`, disables SDK auto-copy/bootstrap for repo files (`.editorconfig`, `global.json`). | | `CurrentYear` | Current year used in generated assembly metadata. | | `AutoGeneratedAssemblyInfoFile` | Relative path to generated AssemblyInfo source file. | Roslyn components additionally expose `LangVersion`, `Nullable`, `TreatWarningsAsErrors`, `EnforceExtendedAnalyzerRules`, `Deterministic`, `ContinuousIntegrationBuild`, and `EmbedUntrackedSources` as compiler-visible properties so downstream tooling can confirm the shipped analyzer packages were built with the standard settings. ## Example: switch a repo to Xunit + NSubstitute and disable Bogus ```xml Acme Xunit NSubstitute None ``` --- # Version Detection The SDK reads the `version` field from a repository-root `package.json` and applies it to both `Version` and `PackageVersion` automatically. When no version source can be resolved (non-strict mode), it silently falls back to `0.0.1`. ## Behaviour 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, locates the repo root from CI workspace variables (`GITHUB_WORKSPACE`, `BUILD_SOURCESDIRECTORY`, `BUILD_REPOSITORY_LOCALPATH`, `CI_PROJECT_DIR`), then by walking up from the project directory looking for a `.git` marker or a `package.json`, and reads `package.json` from there. The extracted `version` field is applied to both `Version` and `PackageVersion`. A build error is raised when a `package.json` was resolved but can't be read, or when it contains no `version` field. With `UsePackageJsonVersion=Strict`, the build also fails when no package.json source can be discovered at all (for example, no explicit `RootPackageJson` and no discoverable `.git` marker or CI workspace variable); in non-strict mode that case silently falls back to the `0.0.1` default. ## Caching Version detection results are cached locally under the user's temporary directory (`%TEMP%\Purview.BuildSdk\VersionDetection`, platform equivalent elsewhere) so repeated evaluations don't re-scan the filesystem. Enable or disable with `EnableVersionDetectionCache` (default `true`). ## Logging Version detection logging is disabled by default. Set `VersionDetectionLogEnabled=true` to emit a high-importance message showing the detected package version. ## Important — set before the import Both `UsePackageJsonVersion` and `RootPackageJson` must be set **before** the `` line in your `Directory.Build.props`. The version logic runs during that import and cannot see properties set afterwards (for example in individual `.csproj` files). ```xml Acme $(MSBuildThisFileDirectory)package.json ``` --- # Assembly Name Generation This page explains the identity-generation rules referenced by [Engineering Principles](../engineering-principles/). 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)`). ## Resolved identities | 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` | Explicitly setting `` before the SDK import always wins: `AssemblyName`/`PackageId` follow that override instead of re-appending a stripped suffix. Test projects keep their detected suffix: `Api.UnitTests` → `AssemblyName`/`PackageId` = `Acme.Api.UnitTests`, while `RootNamespace` remains `Acme.Api`. Explicit `` or `` 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. ## Namespace stripping Certain suffixes are automatically stripped from `RootNamespace` to avoid awkward namespace names like `Acme.MyProject.Core.Something`. The built-in stripping set is broader than the common examples and includes: `SourceGeneration`, `SourceGenerators`, `SourceGenerator`, `ServiceDefaults`, `Infrastructure`, `Abstractions`, `ClientShared`, `Persistence`, `CodeFixers`, `CodeFixes`, `DataAccess`, `Extensions`, `Generators`, `Analyzers`, `Contracts`, `Framework`, `Utilities`, `AppHost`, `Helpers`, `Library`, `Common`, `Shared`, `Infra`, `Utils`, `Core`, `Data`, `Host`, `Util`, `Lib`, `EF`, plus the known shared and shared-testing project names (`Shared*`, `SharedTesting*`). ### Keeping suffixes 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: ```xml ``` This is useful for analyser/source-generator repos that deliberately name projects after the role — for example `Purview.ZodSharp.SourceGenerators`, `Purview.ZodSharp.CodeFixers`, and `Purview.ZodSharp.CodeFixes` — and want those roles to survive in the namespace rather than collapse into the parent `Purview.ZodSharp` namespace. Only suffixes present in the built-in list need removing; a suffix the SDK does not ship is already kept as-is. See [Project Naming Conventions](../project-naming-conventions/) and [InternalsVisibleTo](../internalsvisibleto/) for related naming behaviour. --- # 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 ``** — if a project sets `Custom.Assembly`, the generated attributes use `Custom.Assembly.UnitTests`, `Custom.Assembly.IntegrationTests`, etc. - **Default** — `AssemblyName` is `RootNamespace`-derived, so fully-qualified names are used (for example `Acme.MyProject.UnitTests`). - **`EnableAssemblyNameGeneration=false`** — standard .NET behaviour: `$(MSBuildProjectName)` (for example `MyProject.UnitTests`). ## What is generated 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, so 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 (for example `Acme.SharedTestingFramework`); with `EnableAssemblyNameGeneration=false` the raw name is used. `InternalsVisibleTo` is also generated for `DynamicProxyGenAssembly2` so Moq/NSubstitute dynamic proxies can access internals. ## Disabling automatic InternalsVisibleTo To disable automatic InternalsVisibleTo generation, set `DisableAutoInternalsVisibleTo=true` in your project or `Directory.Build.props`: ```xml true ``` --- # Testing Wiring This page explains how the SDK implements the higher-level testing conventions in [Engineering Principles](../engineering-principles/). The SDK wires the testing stack for you based on three properties that must be set **before** the SDK import: | 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`. | All three are validated at build/restore/pack time, and invalid values fail the build with a clear error. ## TUnit (default) - `OutputType=Exe`, `TestingPlatformDotnetTestSupport=true`, `UseMicrosoftTestingPlatformRunner=true`, `EnableMicrosoftTestingPlatform=true`. - The `TUnit` package is referenced (with the `Microsoft.Testing.Platform` runner configured via `global.json`). - A `[Category: ]` assembly attribute tags every test with its detected test category (for example `Unit`, `Integration`), which makes `--treenode-filter` filtering work. That category is the default classification, not a limit on adding more categories. - `TUnit.Mocks` is referenced when `SubstituteFramework=TUnitMocks` (the default). - `Bogus` is referenced when `TestDataFramework=Bogus` (the default). ## Xunit (opt-in) Set `TestingFramework=Xunit`: - `xunit.v3` and `xunit.runner.visualstudio` (private assets) are referenced. - A `[Trait("Category", "")]` assembly attribute tags every test. - `OutputType=Exe` when the testing framework is not `None`. ## None Set `TestingFramework=None` to disable the test runner wiring entirely. The test project is still detected and gets coverage/IVT behaviour, but no framework packages or runner configuration are added. ## Shared test configuration All test and shared-testing projects get: - A **test-context rule set** (`PurviewTestContextNoWarn`, overridable before the SDK import): the production API-surface rules (`CA1002`, `CA1012`, `CA1034`, `CA1047`, `CA1050`, `CA1051`, `CA1062`, `CA1064`, `CA1515`, `CA1707`) are exempt, because test classes/fixtures are public, test names use `Method_Scenario_Expectation`, and helpers take unvalidated fixture parameters. Style rules (`IDE0040`, field naming, formatting, `IDE1006` naming) are still enforced. `DisablePurviewTestContextRuleSet=true` enforces the production rules too. - `CollectCoverage=true` with coverage exclusions for `[NSubstitute*]`, `[TUnit.*]`, `[xunit.*]`, `[Microsoft.Testing.*]`, `[Microsoft.NET.Test*]`, and `[Bogus*]`. - `ExcludeByAttribute` for `ExcludeFromCodeCoverageAttribute`. - `IsPackable=false`, `IsPublishable=false`, `MaxCpuCount=0`, `DisableGenerateAssemblyInfoClass=true`. - Disabled native instrumentation. - An `[assembly: ExcludeFromCodeCoverage]` attribute. ## Substitution frameworks - **TUnitMocks** (default) — references `TUnit.Mocks`. - **NSubstitute** — references `NSubstitute` plus `NSubstitute.Analyzers.CSharp` (analyzers, private assets) and a global `using NSubstitute`. - **None** — no mocking package. ## Test data frameworks - **Bogus** (default) — references `Bogus` with a global `using Bogus`. - **None** — no data package. ## Shared testing projects Projects named `SharedTestingFramework`, `SharedTestingInfrastructure`, `SharedTestingInfra`, `SharedTestingUtilities`, `SharedTestingUtils`, `SharedTestingLibrary`, `SharedTestingLib`, or `SharedTestingHelpers` are treated as shared testing helpers: - They get the test package references (`TUnit.Core` rather than the full `TUnit`) but **not** the test runner or coverage settings. - A `[Skip]` attribute (TUnit) keeps the shared assembly from being executed directly. - They are forced to `OutputType=Library` (with `IsTestProject`/`IsTestingPlatformApplication` cleared) after package props run, because test packages such as `TUnit.Engine` otherwise turn them into executable test hosts - which would make `CA1515` demand that their fixtures become internal while the test assemblies still consume them. Set `PurviewSharedTestingOutputType=Exe` before the SDK import to opt back into that shape. - Test projects automatically reference the sibling shared testing project via `../SharedTesting*/SharedTesting*.csproj`. ## Automatic project references Test projects automatically reference their target project. The SDK probes `..//`, `../..//`, `../src//`, and `../../src//` (whichever exists), so both the `src/`+`tests/` and flat layouts are covered. ## Running tests Tests use **Microsoft.Testing.Platform** (`global.json` sets `"runner": "Microsoft.Testing.Platform"`) and are filtered with TUnit tree-node filters: ```sh dotnet test --treenode-filter "/*/*/*/*[Category=Unit]/" ``` See [Project Naming Conventions](../project-naming-conventions/) for the recognised test suffixes and [InternalsVisibleTo](../internalsvisibleto/) for how test assemblies access internals. --- # Packaging The SDK configures packable projects (`IsPackable=true`) for clean NuGet output and handles several packaging workflows automatically: symbol packages, Roslyn analyzer assets, source generators, the `Sdk/` folder, and the repository README. ## 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 components — their PDB ships inside the `.nupkg` under `analyzers/dotnet/cs/`). | | `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` | `true` | Includes source files in the package. | 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`). 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. ## Roslyn components (`IsRoslynComponent=true`) Packable Roslyn components automatically pack the built analyzer/generator assembly **and its PDB** into `analyzers/dotnet/cs/` (`PurviewPackAnalyzerPdb=true`). Set `PurviewPackAnalyzerPdb=false` only when symbols are delivered another way — NuGet's `.snupkg` cannot host `analyzers/dotnet/cs` symbols. `IncludeBuildOutput=false` keeps `lib/` empty. Pack-time validation (`ValidateRoslynComponentCompilerSettings`) fails the pack with `PRSGD0001`– `PRSGD0004` if the standard compiler defaults (`LangVersion`, `Nullable`, `TreatWarningsAsErrors`, `EnforceExtendedAnalyzerRules`) are missing. Opt out with `DisableRoslynCompilerDefaultsValidation=true`. Roslyn development dependencies (`Microsoft.CodeAnalysis.CSharp`, `Microsoft.CodeAnalysis.CSharp.Workspaces`, `Microsoft.CodeAnalysis.Analyzers`) default to `PrivateAssets="all"`, and `Microsoft.CodeAnalysis.Analyzers` also gets the analyzer `IncludeAssets`, so they never leak into the packed nuspec. ## Automatic source generator packaging `PackProjectReferencedSourceGenerators=true` (default) automatically packs analyzer `ProjectReference` outputs and their runtime dependencies under `analyzers/dotnet/cs/`. Analyzer project references use the `GetSourceGeneratorAnalyzerFiles` target (set automatically when a `ProjectReference` has `OutputItemType=Analyzer` and `ReferenceOutputAssembly=false`); runtime dependencies are declared as `SourceGeneratorRuntimeDependency` items and copied beside the generator. Duplicates across analyzer projects are deduplicated by package path so the pack never fails on colliding `analyzers/dotnet/cs` targets. Set `PackProjectReferencedSourceGenerators=false` to opt out, or `Pack="false"` on an individual `ProjectReference` to exclude only that generator. ## Automatic `Sdk/` folder packaging (`PurviewAutoSdkPack`) For packable projects, `PurviewAutoSdkPack` (default `true`) automatically adds `Sdk/**/*` as `None` items with `Pack="true"` and `Visible="true"`, mapping each file to the correct location in the package: | Source path | Package path | | -- | -- | | `Sdk/.agents/**` | `.agents/**` | | `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/` | The SDK automatically adds a `.gitignore` file into each second-level folder under `Sdk/.agents` with the content `# Ignore all files\n*\n\n# Don't ignore directories, so Git can traverse them\n!*/\n\n# Keep this file\n!.gitignore`. This ensures the copied folder structure remains discoverable in consuming repositories while the content itself is ignored by Git. MSBuild SDK packages (like `Purview.BuildSdk` itself) set `PurviewAutoSdkPack=false` and pack their `Sdk/` contents explicitly instead. External files linked beneath `Sdk/` (via ``) are packed with the same paths as physical `Sdk/` files. ## Repository README auto-inclusion When the repo root is discoverable (`.git` marker or CI workspace variable), the repository-root `README.md` is packed automatically for packable projects and registered via `PackageReadmeFile` — but only when the file exists and `PackageReadmeFile` has not been configured explicitly. The SDK skips the auto-inclusion if a README-named file is already being packed, so no duplicate readme items are produced. No README is required; if the file is absent the pack succeeds without readme metadata. ## Pack validation The shared `Purview.Build` pipeline runs package validation on pack (`purview-build.json` sets `PackValidation.RequireSymbolPackage=false` for this SDK repo). See [Release Flow](../release-flow/) for the full flow. --- # 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 `` item, so the rules surface in both command-line builds and Visual Studio. | 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 rooted under `Extensions/` | ## Style policy The SDK ships the modifier/visibility **and field-naming** policy in `.editorconfig` and enforces it with `ValidatePurviewStylePolicy` (`PRSGD0006`-`PRSGD0009`), so a repository cannot neuter it: - `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. - Private instance fields must be `_camelCase` (`_name`, never `name`): 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 rule, and local constants stay camelCase. - `CA1515` (public type in an application/test assembly) ships at warning; `CA1852` (seal internal types) is not suppressed for types exposed through `InternalsVisibleTo`; `CA1034` is not disabled for `Extensions/` files; the API-surface rules `CA1062`/`CA1707` are not pre-suppressed for repositories. Where the public surface is framework-mandated the SDK exempts itself: Aspire hosts and CLI apps keep their nested public options types (Spectre.Console settings, `[ZodSchema]` resource-kit options), and test projects use the test-context rule set below. Both exemptions are recorded so `PRSGD0008` accepts them while still rejecting repository-authored silencing. - Test and shared-testing projects are **context aware**: they enforce the same strict style contract as the rest of the repository (`IDE0040`, field naming, formatting, the `IDE1006` naming rules) but not the production API-surface rules. `PurviewTestContextNoWarn` exempts `CA1002`, `CA1012`, `CA1034`, `CA1047`, `CA1050`, `CA1051`, `CA1062`, `CA1064`, `CA1515` and `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. `DisablePurviewTestContextRuleSet=true` opts a repository into the production rules for tests too. - Shared testing projects are helper **libraries** (their test packages request the test-host shape, and the SDK reasserts `OutputType=Library` after package props). Their fixtures stay public API, which is also why `CA1515` does not apply to them; `PurviewSharedTestingOutputType=Exe` opts back in. | Code | Reported when | | -- | -- | | `PRSGD0006` | `dotnet_style_require_accessibility_modifiers` is set to anything other than `omit_if_default`. | | `PRSGD0007` | A policy rule is downgraded below warning (`IDE0040`, `CA1515`, `CA1852`, `CA1034`), any policy rule is set to `none`/`silent`, or the `Style` category is disabled in bulk without an explicit `IDE0040` severity. | | `PRSGD0008` | One of the accessibility rules (`IDE0040`, `CA1515`, `CA1852`, `CA1034`, `CA1012`, `CA1047`, `CA1050`, `CA1051`, `CA1062`, `CA1064`, `CA1707`) is added to `NoWarn` by the repository. Entries the SDK injects itself (the test-context rule set, `CA1515` for Aspire hosts and CLI apps) are ignored. | | `PRSGD0009` | `IDE1006` is hidden, the private instance field naming rule is downgraded below warning, or the `_` prefix is removed from the field naming style. | Opt out with `true`. A stale bootstrapped `.editorconfig` is reported rather than obeyed — refresh it with `PurviewRepoBootstrapMode=Always` (or delete it). ## PDS0002 — Extensions namespace rule 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 (PDS0005) for files in this root `Extensions/` scope. The shipped `.editorconfig` also suppresses the namespace-conflict diagnostics this convention can trigger (`CA1724`, `CS0436`, `CS1591`, `IDE0005`), so extension files never need `#pragma` suppressions. Outside this scope, normal `IDE0130` behaviour remains unchanged. ## PDS0003 — Prefer explicit type with target-typed `new()` Flags `var` declarations that use a target-typed object creation initializer, preferring: ```csharp // ❌ flagged var service = new Service(); // ✅ preferred Service service = new(); ``` ## PDS0004 — Acronym capitalization `PDS0004` follows .NET naming guidance for well-known framework spellings (`Sql`, `Guid`, `Uuid`, `Url`, `Dns`, `Http`, `Xml`, `DbContext`, ...) while still enforcing uppercase for acronyms such as `Api` → `API`, `Ai` → `AI`, `Cpu` → `CPU`, `Gpu` → `GPU`, `Cli` → `CLI`, `Gui` → `GUI`, `Ram` → `RAM`, and `Ssh` → `SSH`. 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. ```ini [*.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; see [Code Fixes](../code-fixes/). --- # Code Fixes The package ships `Purview.BuildSdk.CodeFixers.dll` for the IDE. The code-fix assembly is added as an analyzer reference only when building inside Visual Studio (Roslyn's `CodeFixService` keys on `Project.AnalyzerReferences`, and the command-line compiler cannot resolve `Microsoft.CodeAnalysis.Workspaces`). ## PDS0004 — Rename to correct acronym capitalization The code fix for `PDS0004` renames the identifier to its correct-English capitalization (for example `ApiClient` → `APIClient`) and updates all of its references across the solution. See [Analyzers](../analyzers/) for the full rules and configuration options. ## PDS0002 — Fix extensions namespace When a static extensions class is not at its conventional location (or is at that location but not conventionally named), the **Move extensions class to conventional location** refactoring: - Re-paths the file to `Extensions//Extensions.cs`. - Renames the class to the receiver-derived name (for example an `IServiceCollection` extension becomes `ServiceCollectionExtensions` and moves to `Microsoft.Extensions.DependencyInjection`). - When a `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. ## Split extensions class When a static extensions class targets multiple receiver types, the **Split extensions class into one class per receiver type** refactoring: - Splits the class into one `Extensions` class per receiver — for a generic `this TBuilder where TBuilder : IHostApplicationBuilder` receiver that becomes `HostApplicationBuilderExtensions`. - Places each new file under `Extensions//` so the namespace convention stays satisfied. - When a `Extensions` class already exists in the receiver's namespace, the receiver's methods are merged into it instead of creating a duplicate file. ## Target-typed `new()` The code fix for `PDS0003` rewrites a `var` declaration with a target-typed object creation initializer to use an explicit type with `new()`: ```csharp // before var service = new Service(); // after Service service = new(); ``` --- # Repository Bootstrap The SDK bootstraps repo-level files so external tooling that does not read MSBuild item metadata (for example CSharpier, IDE formatting tools, and SDK resolution) works consistently out of the box. ## `.editorconfig` bootstrapping The package ships an `.editorconfig` in `Sdk/.editorconfig`. It is: 1. Registered on `@(EditorConfigFiles)` via `EditorConfigFilePath` for build-time code-style enforcement (`EnforceCodeStyleInBuild=true`, `EnableNETAnalyzers=true`, `AnalysisLevel=latest`, `AnalysisMode=All`). 2. Written to the repository root (as a physical file) when a `.editorconfig` does not already exist there, so tools like CSharpier pick it up. The shipped file also defines the style policy (`dotnet_style_require_accessibility_modifiers = omit_if_default`, private instance fields named `_camelCase`, with `IDE0040`, `CA1515`, `CA1852` and `CA1034` at warning or above). Because a repository owner can edit a bootstrapped copy, the `ValidatePurviewStylePolicy` target re-checks the *effective* configuration on every compile and fails the build (`PRSGD0006`-`PRSGD0009`) when that policy has been overridden, hidden, or silenced through `NoWarn`. A stale copy that predates the current policy (for example one that still hides `IDE1006`) is therefore reported rather than obeyed — refresh it with `PurviewRepoBootstrapMode=Always`. Opt out with `DisablePurviewStylePolicyValidation=true`; see [Style policy](../analyzers/#style-policy). The write is atomic (a temporary file is renamed into place) and retried quietly, so parallel projects sharing the repository root cannot corrupt it or fail the build by racing. An existing file is never overwritten unless you ask for it. Control it with: | Property | Default | Description | | -- | -- | -- | | `BootstrapEditorConfigToRepoRoot` | `true` | Copies the SDK `.editorconfig` to the repository root when missing. | | `RepositoryEditorConfigFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `.editorconfig`. | | `DisableAutoCopySdkFiles` | `false` | Master switch that disables repo-level SDK file bootstrapping. | | `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing` (never overwrite), `Always` (overwrite), `WarnOnDrift` (warn when the existing file differs from the SDK-provided one) or `Never` (skip all bootstrapping). | | `PurviewRepoBootstrapCopyRetries` | `3` | Write attempts before the failure is reported. | | `PurviewRepoBootstrapCopyRetryDelayMilliseconds` | `500` | Base delay between write attempts. | | `PurviewRepoBootstrapCopyFailureAsError` | `true` | When `false`, a failed bootstrap write is reported as a warning instead of an error. | ## `global.json` bootstrapping The SDK creates a `global.json` at the repository root when one is missing, registering `Purview.BuildSdk` in `msbuild-sdks` and setting the `Microsoft.Testing.Platform` test runner: ```json { "test": { "runner": "Microsoft.Testing.Platform" }, "msbuild-sdks": { "Purview.BuildSdk": "" } } ``` Like the `.editorconfig` bootstrap it writes atomically, retries quietly, never overwrites an existing file, and honours `PurviewRepoBootstrapMode`: | Property | Default | Description | | -- | -- | -- | | `BootstrapGlobalJsonToRepoRoot` | `true` | Creates a `global.json` at the repository root when missing. | | `RepositoryGlobalJsonFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `global.json`. | | `PurviewBuildSdkVersionForGlobalJson` | *(auto-detected or `1.0.0` fallback)* | Version written to the `msbuild-sdks.Purview.BuildSdk` entry. | | `DisableAutoCopySdkFiles` | `false` | Master switch that disables repo-level SDK file bootstrapping. | | `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing`, `Always`, `WarnOnDrift` or `Never`. | ## Retry and failure behaviour Both bootstraps share the same retry contract: | Property | Default | Description | | -- | -- | -- | | `PurviewRepoBootstrapCopyRetries` | `3` | Write attempts before the failure is reported. | | `PurviewRepoBootstrapCopyRetryDelayMilliseconds` | `500` | Base delay between attempts (a small increment is added per attempt). | | `PurviewRepoBootstrapCopyFailureAsError` | `true` | When `false`, a failure is reported as a warning and the build continues. | | `PurviewSuppressCopyRetryWarnings` | `true` | Demotes built-in copy task retry notices (`MSB3026`) to messages. Set to `false` to see every retry. | Retries are therefore silent by design, while a write that never succeeds is still reported - as an error by default, including the destination path and the underlying OS error. ## Repository root discovery Both bootstrap targets locate the repository root by: 1. Running `git rev-parse --show-toplevel` from the `Directory.Build.props` directory. 2. Falling back to probing upward for a `.git` marker. 3. Finally falling back to the `Directory.Build.props` directory itself. Set the `RepositoryEditorConfigFilePath` / `RepositoryGlobalJsonFilePath` properties explicitly to override discovery. --- # Agent Folder `Purview.BuildSdk` ships bundled agent content (skills, prompts, agents) under `.agents/**` in the NuGet package. During build, the SDK copies it into the consuming repository's `.agents/` folder by default so compatible coding agents can discover repository-aware guidance automatically. ## How the copy works - `EnableAgentFolderInPackage=true` (default) syncs the bundled `.agents/**` folder from the SDK NuGet package into `$(AgentPackDestinationFolder)/` (default `.agents`) in the consuming repository **before build**. - The source folder defaults to the package-level `.agents` folder beside `Sdk/` and can be pointed elsewhere with `PurviewAgentFolderSourcePath`. - The destination root is resolved from `RepoRoot` (auto-discovered repo root), an `AGENTS.md` walk-up, or SourceLink source roots. - The sync is also performed for bundled `.agents` folders shipped by **any** restored NuGet package that used `PurviewAutoSdkPack` (read from `project.assets.json`), not just this SDK. - Unchanged content is skipped using a repository manifest (`.purview/agent-sync.cache` by default), so repeat builds do not touch any file. The manifest records a per-file size/timestamp fingerprint plus a content hash, which is what makes an in-place package republish (same version, new content) re-sync. Deleting a mirrored file also re-triggers its copy. - Every write is staged into a temporary file in the destination folder and then renamed into place, so a reader never observes a partially written file and concurrent writers cannot interleave. - Several projects share the same destination, so simultaneous writes are expected. Those retries are logged at low importance only (MSB3026 copy retry notices are demoted to messages) and a copy that still fails after every retry is reported as an **error** by default. ## Failure and retry behaviour | Property | Default | Description | | -- | -- | -- | | `PurviewAgentFolderSourcePath` | *(package-level `.agents`)* | Folder that provides the bundled `.agents` content mirrored into the repository. | | `PurviewAgentFolderCopyRetries` | `3` | Copy attempts per file before the failure is reported. | | `PurviewAgentFolderCopyRetryDelayMilliseconds` | `500` | Base delay between attempts (a small increment is added per attempt). | | `PurviewAgentFolderCopyFailureAsError` | `true` | When `false`, a copy that still fails after every retry is reported as a warning and the build continues. | | `PurviewAgentSyncManifestPath` | `/.purview/agent-sync.cache` | Overrides the change-detection manifest location. | | `PurviewSuppressCopyRetryWarnings` | `true` | Demotes MSB3026 copy retry notices to messages. Set to `false` to see every retry attempt. | ## Opting out To disable bundled agent folder copying in a consuming repo, set the opt-out property before importing the SDK: ```xml false ``` ## Packaging the folder For packable projects, the SDK packs the `Sdk/.agents/**` folder into the package at `.agents/**` and injects a `.gitignore` file into each second-level folder under `Sdk/.agents` with the content `# Ignore all files\n*\n\n# Don't ignore directories, so Git can traverse them\n!*/\n\n# Keep this file\n!.gitignore`. This ensures the copied folder structure remains discoverable in consuming repositories while the content itself is ignored by Git. | 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. | | `EnableAgentFolderInPackage` | `true` | Copies the bundled `.agents/**` folder from the SDK NuGet package into the consuming repository's `.agents/` folder (or `$(AgentPackDestinationFolder)/`) before build. | | `AgentPackDestinationFolder` | `.agents` | Repo-relative destination folder that receives the copied agent folder contents when `EnableAgentFolderInPackage` is `true`. | See [Packaging](../packaging/) for the full `Sdk/` folder packaging rules. --- # Release Flow This repository uses the shared [`Purview.Build`](https://github.com/purview-dev/build) pipeline for the full PR/release cycle, and plain `dotnet`/`just` commands for focused local work. ## Versioning - **`package.json` is the single source of truth for the version.** The `version` field is read by the SDK's [Version Detection](../version-detection/) logic and applied to `Version` and `PackageVersion` for every build and pack. - Current package version: read from `package.json` (`just current_version`). ## Local workflow ```text dotnet tool restore # install local tools (csharpier, etc.) just build # dotnet build src/BuildSdk.slnx --configuration Debug just test # dotnet test with a TUnit tree-node filter just lint-check # csharpier check just lint-fix # csharpier format . just pack # dotnet pack to ./artifacts just pipeline-pr # restore, build, lint, tests, pack, package validation just pipeline-local-release # restore, build, lint, tests, pack, local NuGet publish ``` ## Pipeline workflows | Recipe | Purpose | | -- | -- | | `just pipeline-pr` | PR pipeline — restore, build, lint, tests, pack, package validation. | | `just pipeline-build` | Build pipeline — restore, build, lint, pack, package validation (no tests). | | `just pipeline-tests` | Tests pipeline — restore, build, lint, tests (no pack). | | `just pipeline-release` | Release pipeline — restore, build, lint, tests, pack, publish to NuGet, GitHub release. | | `just pipeline-local-release` | Release pipeline with a local NuGet publish (`--Release:Mode=LocalNuGet`). | The pipeline configuration lives in `purview-build.json`: ```json { "Build": { "Solution": "src/BuildSdk.slnx", "TestRoot": "src/tests", "TestPatterns": "*Tests.csproj", "TestFilter": "/*/*/*/*[Category=Unit]/" }, "PackValidation": { "RequireSymbolPackage": false }, "Release": { "Mode": "None" } } ``` ## CI workflows - **PRs** — `.github/workflows/pr.yml` runs the PR pipeline on pull requests. - **Releases** — `.github/workflows/release.yml` triggers on pushes to `main` and runs the shared `purview-release.yml` workflow with `release-mode: NuGet`, which builds, packs, validates, and publishes the package to NuGet. - `continuousIntegrationBuild` is detected automatically from `CI`, `GITHUB_ACTIONS`, or `TF_BUILD` environment variables for deterministic SourceLink output. ## Testing - Tests use **Microsoft.Testing.Platform** (`global.json` sets `"runner": "Microsoft.Testing.Platform"`) and **TUnit** conventions. - CI runs the full suite on `ubuntu-latest`, so all tests and features must work identically on Windows, Linux, and macOS — never hardcode platform-specific paths in tests or fixtures. - The integration harness (`ProjectHarness`) creates throwaway projects under `Path.GetTempPath()`; see `src/tests/BuildSdk.IntegrationTests/Harness/ProjectHarness.cs`. - Linux agent-pack integration tests can be run locally via `just test-linux` (Docker-based).