dotnet-project-structure

dotnet-project-structure

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dotnet-project-structure" with this command: npx skills add novotnyllc/dotnet-artisan/novotnyllc-dotnet-artisan-dotnet-project-structure

dotnet-project-structure

Reference guide for modern .NET project structure and solution layout. Use when creating new solutions, reviewing existing structure, or recommending improvements.

Prerequisites: Run [skill:dotnet-version-detection] first to determine TFM and SDK version — this affects which features are available (e.g., .slnx requires .NET 9+ SDK).

Scope

  • Solution layout conventions (.sln, src/, tests/)

  • Directory.Build.props and Directory.Build.targets shared config

  • Central Package Management (CPM) and lock files

  • .editorconfig and analyzer configuration

  • SourceLink, NuGet Audit, and nuget.config

Out of scope

  • Build output organization (UseArtifactsOutput) -- see [skill:dotnet-artifacts-output]

  • MSBuild authoring (custom targets, conditions) -- see [skill:dotnet-msbuild-authoring]

Cross-references: [skill:dotnet-project-analysis] for analyzing existing projects, [skill:dotnet-scaffold-project] for generating a new project from scratch, [skill:dotnet-artifacts-output] for centralized build output layout (UseArtifactsOutput ).

Recommended Solution Layout

MyApp/ ├── .editorconfig ├── .gitignore ├── global.json ├── nuget.config ├── Directory.Build.props ├── Directory.Build.targets ├── Directory.Packages.props ├── MyApp.slnx # .NET 9+ SDK / VS 17.13+ ├── src/ │ ├── MyApp.Core/ │ │ └── MyApp.Core.csproj │ ├── MyApp.Api/ │ │ ├── MyApp.Api.csproj │ │ ├── Program.cs │ │ └── appsettings.json │ └── MyApp.Infrastructure/ │ └── MyApp.Infrastructure.csproj └── tests/ ├── MyApp.UnitTests/ │ └── MyApp.UnitTests.csproj └── MyApp.IntegrationTests/ └── MyApp.IntegrationTests.csproj

Key principles:

  • Separate src/ and tests/ directories

  • One project per concern (Core/Domain, Infrastructure, API/Host)

  • Solution file at the repo root

  • All shared build configuration at the repo root

Solution File Formats

.slnx (Modern — .NET 9+)

The XML-based solution format is human-readable and diff-friendly. Requires .NET 9+ SDK or Visual Studio 17.13+.

<Solution> <Folder Name="/src/"> <Project Path="src/MyApp.Core/MyApp.Core.csproj" /> <Project Path="src/MyApp.Api/MyApp.Api.csproj" /> <Project Path="src/MyApp.Infrastructure/MyApp.Infrastructure.csproj" /> </Folder> <Folder Name="/tests/"> <Project Path="tests/MyApp.UnitTests/MyApp.UnitTests.csproj" /> <Project Path="tests/MyApp.IntegrationTests/MyApp.IntegrationTests.csproj" /> </Folder> </Solution>

Convert existing .sln to .slnx :

dotnet sln MyApp.sln migrate

.sln (Legacy — All Versions)

The traditional format remains the fallback for older tooling, CI agents, and third-party integrations that don't support .slnx yet. Keep .sln alongside .slnx during the transition period if needed.

dotnet new sln -n MyApp dotnet sln add src//*.csproj dotnet sln add tests//*.csproj

Directory.Build.props

Shared MSBuild properties applied to all projects in the directory subtree. Place at the repo root.

<Project> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <LangVersion>14</LangVersion> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <AnalysisLevel>latest-all</AnalysisLevel> </PropertyGroup> </Project>

Nested Directory.Build.props

Inner files do not automatically import outer files. To chain them:

<!-- src/Directory.Build.props --> <Project> <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> <PropertyGroup> <!-- src-specific settings --> </PropertyGroup> </Project>

Common pattern: separate props for src vs tests:

repo/ ├── Directory.Build.props # Shared: LangVersion, Nullable, ImplicitUsings ├── src/ │ └── Directory.Build.props # Imports parent + adds TreatWarningsAsErrors └── tests/ └── Directory.Build.props # Imports parent + sets IsTestProject

Directory.Build.targets

Imported after project evaluation. Use for:

  • Shared analyzer package references

  • Custom build targets

  • Conditional logic based on project type

<Project> <!-- Apply analyzers to all projects --> <ItemGroup> <PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" PrivateAssets="all" /> </ItemGroup> </Project>

Central Package Management (CPM)

CPM centralizes all NuGet package versions in Directory.Packages.props at the repo root. Individual .csproj files reference packages without a Version attribute.

Directory.Packages.props

<Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled> </PropertyGroup> <ItemGroup> <!-- Shared dependencies --> <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" /> <PackageVersion Include="System.Text.Json" Version="10.0.0" /> </ItemGroup> <ItemGroup> <!-- Test dependencies --> <PackageVersion Include="xunit.v3" Version="3.2.2" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="coverlet.collector" Version="8.0.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" /> </ItemGroup> </Project>

Project File with CPM

<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <!-- No Version attribute — managed centrally --> <PackageReference Include="Microsoft.Extensions.Logging" /> </ItemGroup> </Project>

Version Overrides

When a specific project needs a different version (rare), use VersionOverride :

<PackageReference Include="Newtonsoft.Json" VersionOverride="13.0.3" />

Flag version overrides during code review — they defeat the purpose of CPM.

.editorconfig

Place at the repo root to enforce consistent code style across all editors and the build.

root = true

[*] indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true

[*.{csproj,props,targets,xml,json,yml,yaml}] indent_size = 2

[*.cs]

Namespace declarations

csharp_style_namespace_declarations = file_scoped:warning

Braces

csharp_prefer_braces = true:warning

var preferences

csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_elsewhere = true:suggestion

Access modifiers

dotnet_style_require_accessibility_modifiers = always:warning

Pattern matching

csharp_style_prefer_pattern_matching = true:suggestion csharp_style_prefer_switch_expression = true:suggestion

Null checking

csharp_style_prefer_null_check_over_type_check = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning

Expression-level preferences

csharp_style_expression_bodied_methods = when_on_single_line:suggestion csharp_style_expression_bodied_properties = true:suggestion

Using directives

csharp_using_directive_placement = outside_namespace:warning dotnet_sort_system_directives_first = true

Naming conventions

dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_underscore dotnet_naming_rule.private_fields_should_be_camel_case.severity = warning dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_accessibilities = private dotnet_naming_style.camel_case_underscore.required_prefix = _ dotnet_naming_style.camel_case_underscore.capitalization = camel_case

See [skill:dotnet-add-analyzers] for full analyzer rule configuration.

global.json

Pin the SDK version for reproducible builds:

{ "sdk": { "version": "10.0.100", "rollForward": "latestPatch" } }

Roll-forward policies:

  • latestPatch — allow patch updates only (recommended for CI)

  • latestFeature — allow feature-band updates within the major version

  • latestMajor — use whatever is installed (development convenience, not for CI)

  • disable — exact version only

nuget.config

Configure package sources and security:

<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <clear /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> </packageSources> <packageSourceMapping> <packageSource key="nuget.org"> <package pattern="*" /> </packageSource> </packageSourceMapping> </configuration>

The <clear />

  • explicit sources + <packageSourceMapping> pattern prevents supply-chain attacks by ensuring packages only come from expected sources.

For private feeds, map internal package prefixes exclusively to the private source:

<packageSources> <clear /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="internal" value="https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json" /> </packageSources> <packageSourceMapping> <packageSource key="nuget.org"> <package pattern="" /> </packageSource> <packageSource key="internal"> <package pattern="MyCompany." /> </packageSource> </packageSourceMapping>

NuGet uses most-specific-pattern-wins precedence: MyCompany.Foo matches MyCompany.* (internal) over * (nuget.org), so internal packages restore exclusively from the private feed. This prevents dependency confusion attacks — an attacker cannot squat MyCompany.Foo on nuget.org because NuGet will never look there for packages matching MyCompany.* .

Do not map the same prefix to multiple sources unless you trust both — that defeats the protection.

NuGet Audit

.NET 9+ enables NuGetAudit by default, which checks for known vulnerabilities during restore. Configure the severity threshold:

<!-- In Directory.Build.props --> <PropertyGroup> <NuGetAudit>true</NuGetAudit> <NuGetAuditLevel>low</NuGetAuditLevel> <NuGetAuditMode>all</NuGetAuditMode> <!-- audit direct + transitive --> </PropertyGroup>

Lock Files

Enable deterministic restores with lock files:

<!-- In Directory.Build.props --> <PropertyGroup> <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> </PropertyGroup>

This generates packages.lock.json per project. Commit these files. In CI, restore with --locked-mode :

dotnet restore --locked-mode

SourceLink and Deterministic Builds

For libraries published to NuGet:

<!-- In Directory.Build.props --> <PropertyGroup> <PublishRepositoryUrl>true</PublishRepositoryUrl> <EmbedUntrackedSources>true</EmbedUntrackedSources> <DebugType>embedded</DebugType> <ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="all" /> </ItemGroup>

Key properties:

  • PublishRepositoryUrl — includes the repo URL in the NuGet package

  • EmbedUntrackedSources — embeds generated source files

  • DebugType=embedded — PDB embedded in the assembly (no separate symbol package needed)

  • ContinuousIntegrationBuild — enables deterministic paths (only in CI to avoid breaking local debugging)

References

  • .NET Library Design Guidance

  • Central Package Management

  • .slnx Format

  • Directory.Build.props

  • SourceLink

  • NuGet Audit

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

dotnet-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-csharp

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-api

No summary provided by upstream source.

Repository SourceNeeds Review