.NET 项目结构和构建配置
何时使用此技能
使用此技能时:
- 设置新的 .NET 解决方案,采用现代最佳实践
- 跨多个项目配置集中式构建属性
- 实施集中式包版本管理
- 设置 SourceLink 以便于调试和 NuGet 包
- 自动化版本管理与发布说明
- 固定 SDK 版本以实现一致构建
相关技能
dotnet-local-tools- 使用 dotnet-tools.json 管理本地 .NET 工具microsoft-extensions-configuration- 配置验证模式
解决方案文件格式 (.slnx)
.slnx 格式是 .NET 9 引入的现代基于 XML 的解决方案文件格式。它取代了传统的 .sln 格式。
与传统 .sln 的优势对比
| 方面 | .sln(传统) | .slnx(现代) |
|---|---|---|
| 格式 | 自定义文本格式 | 标准 XML |
| 可读性 | GUIDs,晦涩的语法 | 清晰,人类可读 |
| 版本控制 | 难以 diff/merge | 易于 diff/merge |
| 编辑 | 需要 IDE | 任何文本编辑器 |
版本要求
| 工具 | 最低版本 |
|---|---|
| .NET SDK | 9.0.200 |
| Visual Studio | 17.13 |
| MSBuild | Visual Studio Build Tools 17.13 |
注意: 从 .NET 10 开始,默认创建 .slnx 文件。在 .NET 9 中,必须显式迁移或指定格式。
示例 .slnx 文件
<Solution>
<Folder Name="/build/">
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
<File Path="global.json" />
<File Path="NuGet.Config" />
<File Path="README.md" />
</Folder>
<Folder Name="/src/">
<Project Path="src/MyApp/MyApp.csproj" />
<Project Path="src/MyApp.Core/MyApp.Core.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/MyApp.Tests/MyApp.Tests.csproj" />
</Folder>
</Solution>
从 .sln 迁移到 .slnx
使用 dotnet sln migrate 命令转换现有解决方案:
# 迁移特定解决方案文件
dotnet sln MySolution.sln migrate
# 或者如果目录中只有一个 .sln,则直接运行:
dotnet sln migrate
重要: 不要在同一个仓库中保留 .sln 和 .slnx 文件。这会导致自动解决方案检测问题,并可能导致同步问题。迁移后,删除旧的 .sln 文件。
你也可以在 Visual Studio 中迁移:
- 打开解决方案
- 在解决方案资源管理器中选择解决方案
- 转到 文件 > 另存为…
- 将 “保存类型” 更改为 Xml 解决方案文件 (*.slnx)
创建新的 .slnx 解决方案
# .NET 10+:默认创建 .slnx
dotnet new sln --name MySolution
# .NET 9:显式指定格式
dotnet new sln --name MySolution --format slnx
# 添加项目(两种格式的工作方式相同)
dotnet sln add src/MyApp/MyApp.csproj
建议
如果您使用的是 .NET 9.0.200 或更高版本,请将您的解决方案迁移到 .slnx。 好处是显著的:
- 显著减少合并冲突(没有随机变化的 GUIDs)
- 人类可读且可在任何文本编辑器中编辑
- 与现代
.csproj格式一致 - 在拉取请求中更好的 diff/审查体验
Directory.Build.props
Directory.Build.props 提供集中式构建配置,适用于目录树中的所有项目。将其放置在解决方案根目录。
完整示例
<Project>
<!-- 元数据 -->
<PropertyGroup>
<Authors>您的团队</Authors>
<Company>贵公司</Company>
<!-- 动态版权年份 - 自动更新 -->
<Copyright>版权所有 © 2020-$([System.DateTime]::Now.Year) 贵公司</Copyright>
<Product>您的产品</Product>
<PackageProjectUrl>https://github.com/yourorg/yourrepo</PackageProjectUrl>
<RepositoryUrl>https://github.com/yourorg/yourrepo</RepositoryUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageTags>your;tags;here</PackageTags>
</PropertyGroup>
<!-- C# 语言设置 -->
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<NoWarn>$(NoWarn);CS1591</NoWarn> <!-- 缺少 XML 注释 -->
</PropertyGroup>
<!-- 版本管理 -->
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
<PackageReleaseNotes>见 RELEASE_NOTES.md</PackageReleaseNotes>
</PropertyGroup>
<!-- 目标框架定义(可重用属性) -->
<PropertyGroup>
<NetStandardLibVersion>netstandard2.0</NetStandardLibVersion>
<NetLibVersion>net8.0</NetLibVersion>
<NetTestVersion>net9.0</NetTestVersion>
</PropertyGroup>
<!-- SourceLink 配置 -->
<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<!-- NuGet 包资产 -->
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)logo.png" Pack="true" PackagePath="\" />
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup>
<PackageIcon>logo.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<!-- 全局使用语句 -->
<ItemGroup>
<Using Include="System.Collections.Immutable" />
</ItemGroup>
</Project>
关键模式
动态版权年份
<Copyright>版权所有 © 2020-$([System.DateTime]::Now.Year) 贵公司</Copyright>
使用 MSBuild 属性函数在构建时插入当前年份。无需手动更新。
可重用目标框架属性
一次定义目标框架,到处引用:
<!-- 在 Directory.Build.props 中 -->
<PropertyGroup>
<NetLibVersion>net8.0</NetLibVersion>
<NetTestVersion>net9.0</NetTestVersion>
</PropertyGroup>
<!-- 在 MyApp.csproj 中 -->
<PropertyGroup>
<TargetFramework>$(NetLibVersion)</TargetFramework>
</PropertyGroup>
<!-- 在 MyApp.Tests.csproj 中 -->
<PropertyGroup>
<TargetFramework>$(NetTestVersion)</TargetFramework>
</PropertyGroup>
SourceLink 用于 NuGet 包
SourceLink 启用 NuGet 包的逐步调试:
<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<!-- 选择适合您的源控制的提供商 -->
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<!-- 或:Microsoft.SourceLink.AzureRepos.Git -->
<!-- 或:Microsoft.SourceLink.GitLab -->
<!-- 或:Microsoft.SourceLink.Bitbucket.Git -->
</ItemGroup>
Directory.Packages.props - 集中包管理
集中包管理(CPM)提供所有 NuGet 包版本的单一真实来源。
设置
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<!-- 为相关包定义版本变量 -->
<PropertyGroup>
<AkkaVersion>1.5.35</AkkaVersion>
<AspireVersion>9.1.0</AspireVersion>
</PropertyGroup>
<!-- 应用程序依赖项 -->
<ItemGroup Label="应用程序依赖项">
<PackageVersion Include="Akka" Version="$(AkkaVersion)" />
<PackageVersion Include="Akka.Cluster" Version="$(AkkaVersion)" />
<PackageVersion Include="Akka.Persistence" Version="$(AkkaVersion)" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
</ItemGroup>
<!-- 构建/工具依赖项 -->
<ItemGroup Label="构建依赖项">
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
</ItemGroup>
<!-- 测试依赖项 -->
<ItemGroup Label="测试依赖项">
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.1" />
<PackageVersion Include="FluentAssertions" Version="7.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.3" />
</ItemGroup>
</Project>
消费包(无需版本)
<!-- 在 MyApp.csproj 中 -->
<ItemGroup>
<PackageReference Include="Akka" />
<PackageReference Include="Akka.Cluster" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<!-- 在 MyApp.Tests.csproj 中 -->
<ItemGroup>
<PackageReference Include="xunit" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
</ItemGroup>
好处
- 单一真实来源 - 所有版本在一个文件中
- 无版本漂移 - 所有项目使用相同版本
- 易于更新 - 一次更改,到处应用
- 分组包 - 相关包的版本变量(例如,所有 Akka 包)
global.json - SDK 版本固定
固定 .NET SDK 版本,以在所有环境中实现一致构建。
{
"sdk": {
"version": "9.0.200",
"rollForward": "latestFeature"
}
}
滚动策略
| 策略 | 行为 |
|---|---|
disable |
需要确切版本 |
patch |
相同主次版本,最新补丁 |
feature |
相同主版本,最新的次.补丁 |
latestFeature |
相同主版本,最新的功能带 |
minor |
相同主版本,最新次版本 |
latestMinor |
相同主版本,最新次版本 |
major |
最新 SDK(不推荐) |
推荐: latestFeature - 允许在同一功能带内进行补丁更新。
版本管理与 RELEASE_NOTES.md
发布说明格式
#### 1.2.0 2025年1月15日 ####
- 添加新功能 X
- 修复 Y 中的错误
- 提高 Z 的性能
#### 1.1.0 2024年12月10日 ####
- 初始发布,功能 A、B、C
解析脚本(getReleaseNotes.ps1)
function Get-ReleaseNotes {
param (
[Parameter(Mandatory=$true)]
[string]$MarkdownFile
)
$content = Get-Content -Path $MarkdownFile -Raw
$sections = $content -split "####"
$result = [PSCustomObject]@{
Version = $null
Date = $null
ReleaseNotes = $null
}
if ($sections.Count -ge 3) {
$header = $sections[1].Trim()
$releaseNotes = $sections[2].Trim()
$headerParts = $header -split " ", 2
if ($headerParts.Count -eq 2) {
$result.Version = $headerParts[0]
$result.Date = $headerParts[1]
}
$result.ReleaseNotes = $releaseNotes
}
return $result
}
版本提升脚本(bumpVersion.ps1)
function UpdateVersionAndReleaseNotes {
param (
[Parameter(Mandatory=$true)]
[PSCustomObject]$ReleaseNotesResult,
[Parameter(Mandatory=$true)]
[string]$XmlFilePath
)
$xmlContent = New-Object XML
$xmlContent.Load($XmlFilePath)
# 更新 VersionPrefix
$versionElement = $xmlContent.SelectSingleNode("//VersionPrefix")
$versionElement.InnerText = $ReleaseNotesResult.Version
# 更新 PackageReleaseNotes
$notesElement = $xmlContent.SelectSingleNode("//PackageReleaseNotes")
$notesElement.InnerText = $ReleaseNotesResult.ReleaseNotes
$xmlContent.Save($XmlFilePath)
}
构建脚本(build.ps1)
# 加载辅助脚本
. "$PSScriptRoot\scripts\getReleaseNotes.ps1"
. "$PSScriptRoot\scripts\bumpVersion.ps1"
# 解析发布说明并更新 Directory.Build.props
$releaseNotes = Get-ReleaseNotes -MarkdownFile (Join-Path -Path $PSScriptRoot -ChildPath "RELEASE_NOTES.md")
UpdateVersionAndReleaseNotes -ReleaseNotesResult $releaseNotes -XmlFilePath (Join-Path -Path $PSScriptRoot -ChildPath "Directory.Build.props")
Write-Output "更新到版本 $($releaseNotes.Version)"
CI/CD 集成
# GitHub Actions 示例
- name: 从发布说明更新版本
shell: pwsh
run: ./build.ps1
- name: 构建
run: dotnet build -c Release
- name: 打包并标记版本
run: dotnet pack -c Release /p:PackageVersion=${{ github.ref_name }}
- name: 推送到 NuGet
run: dotnet nuget push **/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
NuGet.Config
配置 NuGet 源和行为:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<solution>
<add key="disableSourceControlIntegration" value="true" />
</solution>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<!-- 如果需要,添加私有源 -->
<!-- <add key="MyCompany" value="https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json" /> -->
</packageSources>
</configuration>
关键设置:
<clear />- 移除继承的/默认源,以实现可复现的构建disableSourceControlIntegration- 防止 TFS/Git 集成问题
完整项目结构
MySolution/
├── .config/
│ └── dotnet-tools.json # 本地 .NET 工具
├── .github/
│ └── workflows/
│ ├── pr-validation.yml # PR 检查
│ └── release.yml # NuGet 发布
├── scripts/
│ ├── getReleaseNotes.ps1 # 解析 RELEASE_NOTES.md
│ └── bumpVersion.ps1 # 更新 Directory.Build.props
├── src/
│ ├── MyApp/
│ │ └── MyApp.csproj
│ └── MyApp.Core/
│ └── MyApp.Core.csproj
├── tests/
│ └── MyApp.Tests/
│ └── MyApp.Tests.csproj
├── Directory.Build.props # 集中式构建配置
├── Directory.Packages.props # 集中包版本
├── MySolution.slnx # 现代解决方案文件
├── global.json # SDK 版本固定
├── NuGet.Config # 包源配置
├── build.ps1 # 构建协调脚本
├── RELEASE_NOTES.md # 版本历史(由构建解析)
├── README.md # 项目文档
└── logo.png # 包图标
快速参考
| 文件 | 目的 |
|---|---|
MySolution.slnx |
现代 XML 解决方案文件 |
Directory.Build.props |
集中式构建属性 |
Directory.Packages.props |
集中包版本管理 |
global.json |
SDK 版本固定 |
NuGet.Config |
包源配置 |
RELEASE_NOTES.md |
版本历史(由构建解析) |
build.ps1 |
构建协调脚本 |
.config/dotnet-tools.json |
本地 .NET 工具 |