Purview.DotNetProjectSdk
1.0.0-prerelease.23
See the version list below for details.
<Sdk Name="Purview.DotNetProjectSdk" Version="1.0.0-prerelease.23" />
#:sdk Purview.DotNetProjectSdk@1.0.0-prerelease.23
Purview.DotNetProjectSdk
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.
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, IsContainerProject, IsWebSdkProject, IsAspireHostProject, … |
| C# defaults | net10.0 TFM (overridable), LangVersion=preview, Nullable=enable, ImplicitUsings=enable, deterministic builds |
| Code style | .editorconfig baked into the package, applied via EditorConfigFilePath, and auto-bootstrapped to repo root if missing; EnforceCodeStyleInBuild=true |
| Repo bootstrap | Missing repo-root .editorconfig and global.json are auto-copied/created by default (disable via DisableAutoCopySdkFiles=true) |
| CI detection | ContinuousIntegrationBuild set automatically when CI, GITHUB_ACTIONS, or TF_BUILD env vars are present |
| SourceLink | Microsoft.SourceLink.GitHub added to all packable projects (configurable via SourceLinkPackageName) |
| Purview Telemetry | Purview.Telemetry.SourceGenerator + Microsoft.Extensions.Telemetry.Abstractions added by default (opt-out) |
| Assembly info | Auto-generated static class AssemblyInfo with RootNamespace, Version, Company, etc., plus an embedded Microsoft.CodeAnalysis.EmbeddedAttribute (can be excluded via PURVIEW_SDK_EXCLUDE_EMBEDDED). |
| InternalsVisibleTo | Generated for all TestType variants and shared testing projects, using the resolved $(AssemblyName) so explicit, generated, and default naming are all handled |
| Namespace management | NamespacePrefix.ProjectName pattern with suffix stripping (.Core, .Shared, .EF, …) |
| 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.DotNetProjectSdk": "1.0.0"
}
}
2. Create Directory.Build.props at repo root
<Project>
<PropertyGroup>
<NamespacePrefix>YourCompany</NamespacePrefix>
</PropertyGroup>
<Import Sdk="Purview.DotNetProjectSdk" Project="Sdk.props" />
</Project>
3. Create Directory.Build.targets at repo root
<Project>
<Import Sdk="Purview.DotNetProjectSdk" Project="Sdk.targets" />
</Project>
4. Copy Directory.Packages.props to 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=trueis set by the SDK. You must have aDirectory.Packages.propsat your repo root for CPM to work, even if it only contains the packages the SDK adds automatically.
Project naming guide
The SDK applies several conventions automatically based on the .csproj filename and NamespacePrefix.
Defaults (no extra configuration)
By default (EnableAssemblyNameGeneration=false), AssemblyName follows standard .NET behavior — it's just the .csproj filename. RootNamespace is always derived from $(NamespacePrefix).$(ProjectName):
.csproj filename |
AssemblyName |
RootNamespace |
Detected as |
|---|---|---|---|
Api.csproj |
Api |
Acme.Api |
Source project |
Api.UnitTests.csproj |
Api.UnitTests |
Acme.Api |
IsTestProject=true, TestingType=Unit |
Api.IntegrationTests.csproj |
Api.IntegrationTests |
Acme.Api |
IsTestProject=true, TestingType=Integration |
SharedTestingFramework.csproj |
SharedTestingFramework |
Acme.SharedTestingFramework |
IsSharedTestingProject=true |
Note:
InternalsVisibleTofollows$(AssemblyName)— so forApi.csprojthe SDK generatesApi.UnitTests,Api.IntegrationTests, etc.
With EnableAssemblyNameGeneration=true
When enabled, the SDK derives AssemblyName and PackageId from $(PurviewLogicalProjectName) — the full $(NamespacePrefix).$(ProjectName) with deduplication:
.csproj filename |
AssemblyName |
RootNamespace |
|---|---|---|
Api.csproj |
Acme.Api |
Acme.Api |
Api.UnitTests.csproj |
Acme.Api.UnitTests |
Acme.Api |
Core.Infrastructure.csproj |
Acme.Core.Infrastructure |
Acme.Core.Infrastructure |
Use short .csproj names in both modes — 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.
Recommended structure: src/ + tests/
For larger repos, separate source and test projects into src/ and tests/ folders:
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:
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
# 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
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 |
All default package versions with * floating to latest |
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 |
Configuration reference
Set any of these properties before the <Import> in your Directory.Build.props:
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. |
When UsePackageJsonVersion=true (the default) or UsePackageJsonVersion=Strict, the SDK:
- Explicit path — if
RootPackageJsonis set, reads that file directly. - Auto-discovery — otherwise, walks up from the project directory looking for a
.gitmarker to locate the repo root, then readspackage.jsonfrom there.
The extracted version field is applied to both Version and PackageVersion. A build error is raised if the file can't be found or contains no version field. With UsePackageJsonVersion=Strict, the build also fails when no package.json source can be discovered (for example, no explicit RootPackageJson and no discoverable .git marker).
Important — set before the import: Both
UsePackageJsonVersionandRootPackageJsonmust be set before the<Import Sdk="Purview.DotNetProjectSdk" Project="Sdk.props" />line in yourDirectory.Build.props. The version logic runs during that import and cannot see properties set afterwards (e.g. in individual.csprojfiles).<Project> <PropertyGroup> <NamespacePrefix>Acme</NamespacePrefix> <RootPackageJson>$(MSBuildThisFileDirectory)package.json</RootPackageJson> </PropertyGroup> <Import Sdk="Purview.DotNetProjectSdk" 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. |
TargetFramework |
net10.0 |
Override the default TFM per-project or globally. |
SourceLinkPackageName |
Microsoft.SourceLink.GitHub |
SourceLink provider. Set to Microsoft.SourceLink.AzureDevOps.Git for ADO repos. |
EnableAssemblyNameGeneration |
false |
When true, the SDK derives AssemblyName (and PackageId) from $(PurviewLogicalProjectName) — i.e. $(NamespacePrefix).$(ProjectName) with deduplication logic. When false (default), standard .NET behavior applies ($(MSBuildProjectName)). Explicit <AssemblyName> in a .csproj always takes precedence. |
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. Note, when ExcludePurviewTelemetry is false this is excluded 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. |
ProjectSdkTestFramework |
TUnit |
Legacy alias for TestingFramework. |
DisableAutoInternalsVisibleTo |
false |
Set to true to disable automatic InternalsVisibleTo generation for test types and shared testing projects. |
Compiler-visible SDK properties
The SDK now exports its properties via CompilerVisibleProperty, so analyzers and source generators can read them through build_property.<PropertyName>.
| 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. |
ProjectSdkTestFramework |
Legacy alias for TestingFramework. |
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. |
ExcludePurviewTelemetry |
Opt-out for Purview.Telemetry.SourceGenerator. |
ExcludeMSTelemetryExtension |
Opt-out for Microsoft.Extensions.Telemetry.Abstractions. |
DisableGenerateAssemblyInfoClass |
Disables generated AssemblyInfo helper source. |
EnableAssemblyNameGeneration |
When true, the SDK derives AssemblyName from the logical project name. |
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 behavior. |
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. |
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. |
PurviewDotNetProjectSdkVersionForGlobalJson |
Version used for msbuild-sdks.Purview.DotNetProjectSdk when bootstrapping global.json (auto-detected from SDK package path, fallback 1.0.0). |
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. |
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.DotNetProjectSdk" Project="Sdk.props" />
</Project>
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
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.
Shared testing projects
Projects named SharedTestingFramework, SharedTestingInfrastructure, SharedTestingInfra, SharedTestingUtilities, 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 useCustom.Assembly.UnitTests,Custom.Assembly.IntegrationTests, etc. EnableAssemblyNameGeneration=true— the SDK-derived fully-qualified name is used (e.g.Acme.MyProject.UnitTests).- Default — standard .NET behavior:
$(MSBuildProjectName)(e.g.MyProject.UnitTests).
Two categories of friend assemblies are generated:
- TestType variants — one
InternalsVisibleToper definedTestType(Unit,Integration,Architecture,Contract,Functional, …), formatted as$(AssemblyName).{TestType}Tests. - SharedTesting projects — one per known shared testing project name (
SharedTestingFramework,SharedTestingInfrastructure, etc.). WhenEnableAssemblyNameGeneration=trueand aNamespacePrefixis set, these are prefixed (e.g.Acme.SharedTestingFramework); otherwise 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.
Namespace stripping
Certain suffixes are automatically stripped from RootNamespace to avoid awkward namespace names like Acme.MyProject.Core.Something:
Stripped suffixes: Core, EF, Shared, ClientShared, ServiceDefaults, and all shared testing project names.
Assembly name generation
By default (EnableAssemblyNameGeneration=false), the SDK follows standard .NET behavior: AssemblyName is $(MSBuildProjectName). Set EnableAssemblyNameGeneration=true (in Directory.Build.props or individual .csproj) to have the SDK derive AssemblyName from $(PurviewLogicalProjectName):
<PropertyGroup>
<NamespacePrefix>Acme</NamespacePrefix>
<EnableAssemblyNameGeneration>true</EnableAssemblyNameGeneration>
</PropertyGroup>
With this enabled:
| Project name | NamespacePrefix |
Resolved AssemblyName |
|---|---|---|
Api |
Acme |
Acme.Api |
Acme.Api |
Acme |
Acme.Api (no double-prefix) |
Core.Infrastructure |
Acme |
Acme.Core.Infrastructure |
Acme |
Acme |
Acme |
PackageId follows AssemblyName (with namespace-remove patterns applied). An explicit <AssemblyName> in a .csproj always takes precedence over generation.
Note:
RootNamespaceis derived from$(PurviewLogicalProjectName)regardless of this setting — it always reflects$(NamespacePrefix).$(ProjectName)with suffix stripping applied.EnableAssemblyNameGenerationonly controls whetherAssemblyName/PackageIdfollow suit.
Central Package Management
The SDK sets ManagePackageVersionsCentrally=true. The templates/Directory.Packages.props file contains PackageVersion entries for all packages the SDK auto-adds — all set to Version="*" (floating to latest).
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.
Building the SDK
dotnet build src/DotNetProjectSdk.slnx -c Release
dotnet test src/DotNetProjectSdk.slnx -c Release
dotnet pack src/src/DotNetProjectSdk/DotNetProjectSdk.csproj -o ./artifacts
License
MIT
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-prerelease.29 | 0 | 8/2/2026 |
| 1.0.0-prerelease.28 | 38 | 7/31/2026 |
| 1.0.0-prerelease.27 | 35 | 7/30/2026 |
| 1.0.0-prerelease.26 | 57 | 7/21/2026 |
| 1.0.0-prerelease.25 | 55 | 7/21/2026 |
| 1.0.0-prerelease.24 | 49 | 7/19/2026 |
| 1.0.0-prerelease.23 | 56 | 7/16/2026 |
| 1.0.0-prerelease.22 | 44 | 7/16/2026 |
| 1.0.0-prerelease.21 | 54 | 7/15/2026 |
| 1.0.0-prerelease.20 | 51 | 7/15/2026 |
| 1.0.0-prerelease.19 | 53 | 7/14/2026 |
| 1.0.0-prerelease.18 | 52 | 7/13/2026 |
| 1.0.0-prerelease.17 | 56 | 7/8/2026 |
| 1.0.0-prerelease.16 | 58 | 7/4/2026 |
| 1.0.0-prerelease.15 | 74 | 6/22/2026 |
| 1.0.0-prerelease.14 | 64 | 6/22/2026 |
| 1.0.0-prerelease.13 | 68 | 6/22/2026 |
| 1.0.0-prerelease.12 | 71 | 6/22/2026 |
| 1.0.0-prerelease.11 | 62 | 6/20/2026 |
| 1.0.0-prerelease.10 | 63 | 6/20/2026 |