dotnet-project-structure

📁 novotnyllc/dotnet-artisan 📅 3 days ago
3
总安装量
3
周安装量
#62632
全站排名
安装命令
npx skills add https://github.com/novotnyllc/dotnet-artisan --skill dotnet-project-structure

Agent 安装分布

opencode 3
github-copilot 3
codex 3
kimi-cli 3
gemini-cli 3
cursor 3

Skill 文档

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