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 包中包含仓库 URLEmbedUntrackedSources— 嵌入生成的源文件DebugType=embedded— PDB 嵌入在程序集中(无需单独的符号包)ContinuousIntegrationBuild— 启用确定性路径(仅在 CI 中,以避免破坏本地调试)