Engineering Principles
Engineering Principles
Section titled “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:
Core principles
Section titled “Core principles”- Naming is part of configuration.
- Folder placement is part of configuration.
NamespacePrefixis the root identity source for the repository.RootNamespaceis the canonical code identity by default.AssemblyNameandPackageIdshould 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
Section titled “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:
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.mdCanonical 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
Section titled “Project naming rules”Use short project names. The SDK applies the prefixing and identity generation for you.
Good examples:
Hosting.csprojIdentity.API.csprojDomain.UnitTests.csprojSharedTestingInfra.csproj
Avoid redundant prefixing:
Aspire.Hosting.csprojAcme.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:
SharedSharedFrameworkSharedInfrastructureSharedInfraSharedUtilitiesSharedUtilsSharedLibrarySharedLibSharedHelpers
Recognised shared testing project names:
SharedTestingFrameworkSharedTestingInfrastructureSharedTestingInfraSharedTestingUtilitiesSharedTestingUtilsSharedTestingLibrarySharedTestingLibSharedTestingHelpers
Namespace, assembly, and package identity
Section titled “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:
RootNamespacedefaults to the logical project identity.- Duplicate tail segments are collapsed, so an already-prefixed project does not get double-prefixed.
- Test suffixes such as
.UnitTestsand.IntegrationTestsare removed fromRootNamespace. - Known non-identity suffixes such as
Core,EF,Shared,ClientShared, andServiceDefaultsmay be stripped fromRootNamespace. AssemblyNameandPackageIdnormally default to the resolved project identity.- When suffix stripping would otherwise collapse two distinct artifacts into the same identity,
AssemblyNameandPackageIdkeep 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
Section titled “Automatic project references”The SDK infers project references from naming and placement.
For test projects:
TargetProjectNameis 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
Section titled “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:
UnitTestsIntegrationTestsE2ETestsFunctionalTestsContractTests
The SDK also recognises broader suffixes when a repository genuinely needs them, including:
AcceptanceTestsPerformanceTestsLoadTestsSmokeTestsStressTestsRegressionTestsSecurityTestsScenarioTestsSystemTestsArchitectureTestsAccessibilityTestsInteractiveTestsEnvironmentTestsWhiteBoxTestsBlackBoxTestsChaosTestsThreatTests
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
Section titled “Default and specialised test wiring”Standard test projects automatically receive the default test stack from the SDK:
TUnitTUnit.MocksBogus- Microsoft.Testing.Platform integration
Shared testing projects receive test-support wiring rather than a runnable test host.
Specialised dependencies stay explicit and intentional:
TUnit.Aspirefor Aspire lifecycle or AppHost-backed integration testsTestcontainersfor 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
Section titled “Test readability rules”The goal of the naming conventions is readability when a repository contains thousands of tests.
Subject-based tests
Section titled “Subject-based tests”When a test class owns a specific subject, prefer {SubjectName}Tests.
Examples:
CustomerIdTestsResultsEndpointFilterTestsAssemblyNameCalculatorTests
When a test method targets a specific member or subject behavior, use:
{SubjectOrMemberUnderTest}_{Scenario}_{Expectation}
Examples:
Create_GivenInvalidEmail_ThrowsArgumentExceptionConstructor_GivenNullLogger_ThrowsArgumentNullExceptionDisplayName_WhenTrimmed_ReturnsNormalizedValueCompareTo_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
Section titled “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:
BuildIntegrationTestsGeneratedPackageAssetsTestsCrossPlatformSchemaCompatibilityTestsAppHostLifecycleTests
Use TUnit features such as display names, categories, and data-driven metadata to keep these broader suites discoverable and understandable.
Async, assertions, and cancellation
Section titled “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 acceptCancellationToken cancellationTokenas 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
Section titled “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.