purview-buildsdk-project-layout
Decide where a project goes and what to call it in a repository that uses Purview.BuildSdk. Use when creating, moving, splitting or renaming a project or test project, choosing a test suffix, aligning
SKILL.mdskills/purview-buildsdk-project-layout/SKILL.md
GitHubProject layout and naming for Purview.BuildSdk
Use this skill when physically adding, moving, splitting or renaming a project.
For why the SDK classified an existing project a particular way, use
purview-buildsdk-project-detection. For the property that changes a behaviour, use
purview-buildsdk-configuration.
First principle
Treat naming and placement as configuration, not decoration.
The SDK infers namespaces, assembly identity, test categories, package wiring and project references from a small set of conventions. The closer a repository sits to them, the fewer overrides it needs.
The corollary matters just as much: preserve the host repository's existing layout first. Only introduce new structure when no established pattern exists.
Canonical layout
<repo root>/
package.json version source
.editorconfig bootstrapped by the SDK
skills/ authored agent skills, if any
src/
<Solution>.slnx
src/<Project>/<Project>.csproj
tests/<Target>.<Type>Tests/<Target>.<Type>Tests.csproj
- The
.csprojfilename must equal its containing directory name, unlessDisableProjectFileNamingConventionCheck=true. - Source projects under
src/, test projects undertests/. - Place a new project beside similar ones — same language, layer and test type.
- One test type per project by default.
A repository that already uses a different shape keeps it; add to it rather than converting it.
Naming and identity
NamespacePrefix is the root identity source. Keep project names short and let the SDK apply the
prefix:
NamespacePrefix |
Project | RootNamespace |
|---|---|---|
Aspire |
Hosting |
Aspire.Hosting |
Acme.Sales.RegionalPipeline |
Identity.API |
Acme.Sales.RegionalPipeline.Identity.API |
Acme.Sales.RegionalPipeline |
Identity.Core |
Acme.Sales.RegionalPipeline.Identity |
AssemblyName and PackageId follow the resolved RootNamespace — except where suffix-stripping
removed a segment (Core, Shared, ServiceDefaults), in which case they keep the fuller logical
name so the artifacts stay distinct from their parent. Explicit values always win, and
EnableAssemblyNameGeneration=false (before the SDK import) restores standard behaviour.
Do not hand-author an alternate namespace scheme when one is already in use.
Test project naming
Test detection is a pattern, not a list: ^.*\.([\w|_]\w*)Tests?$. A target name, a dot, then
any word before Test or Tests. The captured word becomes TestingType and the baseline category.
The dot is required. A project named plainly UnitTests, with no target in front of it, is not
detected as a test project at all — no runner, no coverage, no InternalsVisibleTo, no category.
Always name a test project <Target>.<Type>Tests.
Prefer these by default: UnitTests, IntegrationTests, E2ETests, FunctionalTests,
ContractTests. Use another suffix only when the test type itself carries operational meaning.
A suffix outside the SDK's shipped TestType group still builds, runs and gets its category — it
just cannot reach the target project's internal members, because that group drives
InternalsVisibleTo generation. The full list is in purview-buildsdk-project-detection.
Shared projects: exact names only
The SDK recognises these names exactly — a near miss gets none of the behaviour.
Shared: Shared, SharedFramework, SharedInfrastructure, SharedInfra, SharedUtilities,
SharedUtils, SharedLibrary, SharedLib, SharedHelpers.
Shared testing: SharedTestingFramework, SharedTestingInfrastructure, SharedTestingInfra,
SharedTestingUtilities, SharedTestingUtils, SharedTestingLibrary, SharedTestingLib,
SharedTestingHelpers.
Automatic project references to preserve
Layout is what makes these work. Move a project off the convention and you own the reference.
Test projects probe for their target, in order:
../$(TargetProjectName)/$(TargetProjectName).csproj../../$(TargetProjectName)/$(TargetProjectName).csproj../src/$(TargetProjectName)/$(TargetProjectName).csproj../../src/$(TargetProjectName)/$(TargetProjectName).csproj
Non-test projects pick up sibling shared projects via ../Shared*/Shared*.csproj; test projects
pick up ../SharedTesting*/SharedTesting*.csproj.
Creating a new project
- Match the SDK and style of sibling projects.
- Use Central Package Management if the repository has it.
- Add only the dependencies the project's scope needs — the SDK supplies the testing stack, SourceLink and telemetry by default.
- Add the project to the solution.
- Keep target frameworks, nullability, analyzers and warning settings consistent with neighbours.
Moving or splitting
- Create the destination using the established layout.
- Move the files.
- Update namespaces, usings and references for the destination.
- Verify the destination still produces the intended
TestingType,TargetProjectNameandRootNamespace— ask MSBuild rather than assuming:dotnet msbuild <project> -getProperty:RootNamespace -getProperty:TargetProjectName. - Remove dependencies the source no longer needs.
- Update solution membership and project references.
- Build, then run the affected tests.
Guardrails
- Prefer minimal, targeted diffs; avoid cross-cutting renames unrelated to the move.
- Keep test intent unchanged while relocating.
- When the structure is ambiguous, infer from the nearest sibling projects and say which assumption you made.
- When in doubt, preserve compatibility with the SDK's automatic naming, namespace and project-reference behaviour.