.NET项目结构 dotnet-project-structure

这个技能提供现代 .NET 项目结构和解决方案布局的参考指南。它涵盖如何使用 .slnx 文件、Directory.Build.props 进行共享配置、中央包管理(CPM)、.editorconfig 强制代码风格、分析器集成、NuGet 安全等功能,以标准化项目布局、优化构建流程和提升代码质量。关键词:.NET 开发、项目结构、解决方案布局、构建配置、代码质量管理、DevOps 实践、NuGet 管理。

DevOps 0 次安装 0 次浏览 更新于 3/6/2026

name: dotnet-project-structure description: “设置 .NET 解决方案布局。.slnx、Directory.Build.props、CPM、.editorconfig、分析器。” user-invocable: false

.NET 项目结构

参考指南,用于现代 .NET 项目结构和解决方案布局。在创建新解决方案、审查现有结构或推荐改进时使用。

先决条件: 首先运行 [skill:dotnet-version-detection] 以确定 TFM 和 SDK 版本 — 这影响哪些功能可用(例如,.slnx 需要 .NET 9+ SDK)。

范围

  • 解决方案布局约定(.sln, src/, tests/)
  • Directory.Build.props 和 Directory.Build.targets 共享配置
  • 中央包管理(CPM)和锁文件
  • .editorconfig 和分析器配置
  • SourceLink、NuGet 审计和 nuget.config

不在范围内

  • 构建输出组织(UseArtifactsOutput)-- 参见 [skill:dotnet-artifacts-output]
  • MSBuild 编写(自定义目标、条件)-- 参见 [skill:dotnet-msbuild-authoring]

交叉引用: [skill:dotnet-project-analysis] 用于分析现有项目,[skill:dotnet-scaffold-project] 用于从头生成新项目,[skill:dotnet-artifacts-output] 用于集中构建输出布局(UseArtifactsOutput)。


推荐解决方案布局

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

关键原则:

  • 分离 src/tests/ 目录
  • 每个关注点一个项目(Core/Domain、Infrastructure、API/Host)
  • 解决方案文件在仓库根目录
  • 所有共享构建配置在仓库根目录

解决方案文件格式

.slnx(现代 — .NET 9+)

基于 XML 的解决方案格式,易于阅读和差异友好。需要 .NET 9+ SDK 或 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>

转换现有 .sln.slnx

dotnet sln MyApp.sln migrate

.sln(传统 — 所有版本)

传统格式,适用于旧工具、CI 代理和尚未支持 .slnx 的第三方集成。如果需要,在过渡期间保留 .sln.slnx 一起。

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

Directory.Build.props

共享的 MSBuild 属性,应用于目录子树中的所有项目。放置在仓库根目录。

<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>

嵌套 Directory.Build.props

内部文件不自动导入外部文件。要链接它们:

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

常见模式:为 src 和 tests 分离 props:

repo/
├── Directory.Build.props              # 共享:LangVersion、Nullable、ImplicitUsings
├── src/
│   └── Directory.Build.props          # 导入父级 + 添加 TreatWarningsAsErrors
└── tests/
    └── Directory.Build.props          # 导入父级 + 设置 IsTestProject

Directory.Build.targets

在项目评估后导入。用于:

  • 共享分析器包引用
  • 自定义构建目标
  • 基于项目类型的条件逻辑
<Project>
  <!-- 对所有项目应用分析器 -->
  <ItemGroup>
    <PackageReference Include="Meziantou.Analyzer" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" PrivateAssets="all" />
  </ItemGroup>
</Project>

中央包管理(CPM)

CPM 将所有 NuGet 包版本集中到仓库根目录的 Directory.Packages.props 中。单个 .csproj 文件引用包时不含 Version 属性。

Directory.Packages.props

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
  </PropertyGroup>
  <ItemGroup>
    <!-- 共享依赖 -->
    <PackageVersion Include="Microsoft.Extensions.Logging" Version="10.0.0" />
    <PackageVersion Include="System.Text.Json" Version="10.0.0" />
  </ItemGroup>
  <ItemGroup>
    <!-- 测试依赖 -->
    <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>

使用 CPM 的项目文件

<Project Sdk="Microsoft.NET.Sdk">
  <ItemGroup>
    <!-- 无 Version 属性 — 集中管理 -->
    <PackageReference Include="Microsoft.Extensions.Logging" />
  </ItemGroup>
</Project>

版本覆盖

当特定项目需要不同版本时(罕见),使用 VersionOverride

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

在代码审查中标记版本覆盖 — 它们破坏了 CPM 的目的。


.editorconfig

放置在仓库根目录,以在所有编辑器和构建中强制一致的代码风格。

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]
# 命名空间声明
csharp_style_namespace_declarations = file_scoped:warning

# 大括号
csharp_prefer_braces = true:warning

# var 偏好
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion

# 访问修饰符
dotnet_style_require_accessibility_modifiers = always:warning

# 模式匹配
csharp_style_prefer_pattern_matching = true:suggestion
csharp_style_prefer_switch_expression = true:suggestion

# null 检查
csharp_style_prefer_null_check_over_type_check = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning

# 表达式级偏好
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = true:suggestion

# using 指令
csharp_using_directive_placement = outside_namespace:warning
dotnet_sort_system_directives_first = true

# 命名约定
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

参见 [skill:dotnet-add-analyzers] 获取完整分析器规则配置。


global.json

固定 SDK 版本以实现可重复构建:

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

滚动向前策略:

  • latestPatch — 仅允许补丁更新(推荐用于 CI)
  • latestFeature — 允许主要版本内的功能带更新
  • latestMajor — 使用已安装的任何版本(开发便利,不用于 CI)
  • disable — 仅精确版本

nuget.config

配置包源和安全:

<?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>

<clear /> + 显式源 + <packageSourceMapping> 模式通过确保包仅来自预期源来防止供应链攻击。

对于私有源,将内部包前缀专门映射到私有源:

<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 使用 最特定模式优先 的优先级:MyCompany.Foo 匹配 MyCompany.*(内部)而非 *nuget.org),因此内部包专门从私有源恢复。这防止了依赖混淆攻击 — 攻击者无法在 nuget.org 上抢注 MyCompany.Foo,因为 NuGet 永远不会在那里查找匹配 MyCompany.* 的包。

不要 将相同前缀映射到多个源,除非您信任两者 — 那会破坏保护。


NuGet 审计

.NET 9+ 默认启用 NuGetAudit,在恢复期间检查已知漏洞。配置严重性阈值:

<!-- 在 Directory.Build.props 中 -->
<PropertyGroup>
  <NuGetAudit>true</NuGetAudit>
  <NuGetAuditLevel>low</NuGetAuditLevel>
  <NuGetAuditMode>all</NuGetAuditMode>  <!-- 审计直接 + 传递 -->
</PropertyGroup>

锁文件

启用确定性恢复与锁文件:

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

这生成每个项目的 packages.lock.json 文件。提交这些文件。在 CI 中,使用 --locked-mode 恢复:

dotnet restore --locked-mode

SourceLink 和确定性构建

对于发布到 NuGet 的库:

<!-- 在 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>

关键属性:

  • PublishRepositoryUrl — 在 NuGet 包中包含仓库 URL
  • EmbedUntrackedSources — 嵌入生成的源文件
  • DebugType=embedded — PDB 嵌入在程序集中(无需单独的符号包)
  • ContinuousIntegrationBuild — 启用确定性路径(仅在 CI 中,以避免破坏本地调试)

参考文献