Skip to content
Purview Dev logo

Project Naming Conventions

Stable Build SDK Reviewed 2026-10-12 purview-dev/build-sdk Star on GitHub 1 central-package-management code-style csharp developer-experience developer-tooling devex dotnet msbuild msbuild-sdk nuget project-templates roslyn roslyn-analyzers roslyn-generators tunit xunit

This page explains the mechanics behind the higher-level guidance in 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.

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 filenameAssemblyName / PackageIdRootNamespaceDetected as
Api.csprojAcme.ApiAcme.ApiSource project
Api.UnitTests.csprojAcme.Api.UnitTestsAcme.ApiIsTestProject=true, TestingType=Unit
Api.IntegrationTests.csprojAcme.Api.IntegrationTestsAcme.ApiIsTestProject=true, TestingType=Integration
SharedTestingFramework.csprojAcme.SharedTestingFrameworkAcmeIsSharedTestingProject=true

Note: InternalsVisibleTo follows $(AssemblyName) — so for Api.csproj the SDK generates Acme.Api.UnitTests, Acme.Api.IntegrationTests, etc.

Use short .csproj names — the SDK handles the prefixing:

✅ Api.csproj → short name, SDK resolves the rest
❌ Acme.Api.csproj → redundant prefix, avoid

A build-time check (PurviewProjectFileNameMismatch) enforces that the .csproj filename matches its parent directory name, preventing inconsistent naming. Set DisableProjectFileNamingConventionCheck=true to opt out.

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

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.

Terminal window
# 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 projects are automatically detected by their suffix. The common recommended set is:

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

The detected test type becomes the baseline category for the project. Additional categories can still be added when they improve discoverability or filtering.

Detection itself is a pattern, not a lookup: the project name is matched against ^.*\.([\w|_]\w*)Tests?$ and the captured word becomes TestingType. So MyProject.GoldenTests is detected exactly as readily as MyProject.UnitTests, and gets TestingType=Golden and Category=Golden.

The name needs a target in front of it. The pattern requires the dot, so a project named plainly UnitTests matches nothing: it is not detected as a test project at all, and gets no runner, no coverage and no category. Name it <Target>.UnitTests.

What the SDK does ship as an explicit list is the TestType item group, and it governs InternalsVisibleTo generation rather than detection — a non-test project grants access to {AssemblyName}.{TestType}Tests for each name on it. A suffix outside that list still builds, still runs and still gets its category; it just cannot reach the target project’s internal members. The full set is listed in Project Type Detection.

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 for how these names are classified, and Assembly Name Generation for how the identities are derived.