Skip to content
Go back

在 .NET 中掌握 Directory.Build.props

在 .NET 中掌握 Directory.Build.props

引言

每一位 .NET 开发者最终都会遇到那堵“墙”:

当你想要调整一个设置时,你不得不花费大量时间在整个解决方案中逐个修改。

在 .NET 9(以及任何现代 SDK 风格的项目)中,有一个更好的方法:使用 Directory.Build.props

通过这一个文件,你可以:

本文将带你深入了解 Directory.Build.props 的工作原理,并通过一个实战场景展示如何利用它来清理和优化你的 .NET 解决方案。

什么是 Directory.Build.props?

Directory.Build.props 是一个 MSBuild 文件,MSBuild 会在加载你的项目文件之前自动导入它。你在其中定义的任何属性和项(Items)都将应用于该目录及其子目录下的所有项目。

导入过程的工作原理如下:

  1. 当 MSBuild 加载一个项目(例如 Api.csproj)时,它首先导入 Microsoft.Common.props
  2. Microsoft.Common.props 会从项目所在的文件夹开始,沿着目录树向上查找,寻找第一个 Directory.Build.props 文件。
  3. 一旦找到,它就会导入该文件。
  4. Directory.Build.props 中定义的任何内容现在都可以在项目文件中使用了。

这意味着:

这在 .NET 9 中的工作方式与之前的现代 SDK 版本相同,但区别在于 .NET 9 项目通常更依赖于分析器、可空引用类型和现代构建特性,这使得集中化配置变得更加有价值。

实战场景:清理 .NET 9 微服务解决方案

想象一下,你正在处理一个结构如下的真实解决方案:

src/
    Api/
        Api.csproj
    Worker/
        Worker.csproj
    Web/
        Web.csproj
tests/
    Api.Tests/
        Api.Tests.csproj
    Worker.Tests/
        Worker.Tests.csproj
    Shared.Testing/
        Shared.Testing.csproj
Directory.Build.props
tests/Directory.Build.props

典型问题:

如果你想将 TargetFrameworknet8.0 更改为 net9.0,或者决定加强分析器规则,你必须手动更改每个 .csproj

让我们用 Directory.Build.props 来解决这个问题。

步骤 1:创建解决方案级 Directory.Build.props

在解决方案根目录下,创建一个名为 Directory.Build.props 的文件:

<Project>
  <!-- 仓库中所有 .NET 9 项目的共享配置 -->
  <PropertyGroup>

    <!-- 目标框架与语言特性 -->
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <LangVersion>latest</LangVersion>

    <!-- 代码质量 -->
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <AnalysisLevel>latest</AnalysisLevel>

    <!-- 程序集元数据 -->
    <Company>TheCodeMan</Company>
    <Authors>Stefan Djokic</Authors>
    <RepositoryUrl>https://github.com/thecodeman/your-repo</RepositoryUrl>
    <RepositoryType>git</RepositoryType>

    <!-- 输出布局 -->
    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
    <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
    <BaseOutputPath>artifacts\bin\</BaseOutputPath>
    <BaseIntermediateOutputPath>artifacts\obj\</BaseIntermediateOutputPath>

  </PropertyGroup>

  <!-- 大多数项目共享的包 -->
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
  </ItemGroup>

</Project>

这带来了什么改变?

现在,你原本臃肿的 .csproj 文件可以变得非常精简:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <AssemblyName>MyCompany.Api</AssemblyName>
  </PropertyGroup>
</Project>

更整洁,更容易审查,也更难配置错误。

注意:对于包管理本身(集中版本号),在现代 .NET 中通常会搭配 Directory.Packages.props 使用。Directory.Build.props 主要用于构建配置,虽然它可以包含 PackageReference 作为默认值,但不是专门用于集中包版本管理的。

步骤 2:添加测试专用的 Directory.Build.props

现在让我们区别对待测试项目:它们通常需要额外的包和稍微宽松的规则。

创建 tests/Directory.Build.props

<Project>
  <!-- 对于测试项目,此文件会在根目录的 Directory.Build.props 之后导入 -->

  <PropertyGroup>

    <!-- 可选:测试可能不需要将所有警告视为错误 -->
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>

    <!-- 将这些程序集标记为测试项目 -->
    <IsTestProject>true</IsTestProject>

  </PropertyGroup>

  <ItemGroup>

    <!-- 共享的测试库 -->
    <PackageReference
        Include="xunit"
        Version="2.9.0" />

    <PackageReference
        Include="xunit.runner.visualstudio"
        Version="2.8.2" />

    <PackageReference
        Include="FluentAssertions"
        Version="6.12.0" />

    <PackageReference
        Include="coverlet.collector"
        Version="6.0.0">
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>

  </ItemGroup>

</Project>

现在的效果:

步骤 3:使用 Directory.Build.props 进行集中版本控制

另一个强大的用例是集中程序集版本控制。与其在每个项目中重复版本信息,不如定义一次,并通过 MSBuild 属性传递 CI 元数据。

扩展根目录的 Directory.Build.props

<Project>
  <PropertyGroup>

    <!-- 整个解决方案的基础语义版本 -->
    <VersionPrefix>1.4.0</VersionPrefix>

    <!-- 可选的手动设置后缀,用于预发布版本 -->
    <VersionSuffix>beta</VersionSuffix>
    <!-- 例如 "", "beta", "rc1" -->

    <!-- 程序集版本 -->
    <AssemblyVersion>1.4.0.0</AssemblyVersion>

    <!-- FileVersion 可以包含来自 CI 的构建号 -->
    <FileVersion>1.4.0.$(BuildNumber)</FileVersion>

    <!-- InformationalVersion 是你在“产品版本”和 NuGet 中看到的 -->
    <InformationalVersion>$(VersionPrefix)-$(VersionSuffix)+build.$(BuildNumber)</InformationalVersion>

  </PropertyGroup>
</Project>

在 CI(GitHub Actions / Azure DevOps / GitLab)中,你可以传递 BuildNumber

dotnet build MySolution.sln /p:BuildNumber=123

结果:

步骤 4:集中强制代码风格和分析器

你可能已经在使用 .editorconfig 了。但是分析器和构建级别的强制执行仍然存在于 MSBuild 中。

Directory.Build.props 是连接它们的完美场所:

<Project>
  <PropertyGroup>

    <!-- 全局强制执行分析器 -->
    <AnalysisLevel>latest</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>

    <!-- 将所有分析器诊断视为错误(除非被覆盖) -->
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

  </PropertyGroup>

  <ItemGroup>

    <!-- 示例分析器包 -->
    <PackageReference
        Include="Roslynator.Analyzers"
        Version="4.12.0"
        PrivateAssets="all" />

    <PackageReference
        Include="SerilogAnalyzer"
        Version="0.15.0"
        PrivateAssets="all" />

  </ItemGroup>

</Project>

现在:

如果某个特定项目确实需要放宽某些限制,你仍然可以在本地覆盖:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>

    <!-- 选择退出此项目的全局警告即错误设置 -->
    <TreatWarningsAsErrors>false</TreatWarningsAsErrors>

  </PropertyGroup>
</Project>

步骤 5:分层与作用域:多个 Directory.Build.props 文件

你不局限于只有一个 props 文件。MSBuild 允许你创建一个 Directory.Build.props 文件的层级结构,它会在从项目位置向上遍历目录时选取它找到的第一个文件。

常见模式:

示例结构:

Directory.Build.props         // 全局默认值
src/Directory.Build.props     // 生产代码的覆盖
tests/Directory.Build.props   // 测试代码的覆盖
tools/Directory.Build.props   // 小型内部工具的覆盖

一个 src/Directory.Build.props 可能看起来像这样:

<Project>
  <PropertyGroup>

    <!-- 仅用于生产代码 -->
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>

    <NoWarn>CS1591</NoWarn>
    <!-- 但不要求到处都有 XML 文档 -->

  </PropertyGroup>
</Project>

如果你真的需要一个项目不继承任何根级别的 props,你可以在该项目文件夹中创建一个空的 Directory.Build.props

<Project>
  <!-- 故意留空以停止从父级 props 继承 -->
</Project>

这是因为 MSBuild 在从项目目录向上遍历时,会在找到第一个 Directory.Build.props 时停止。

Directory.Build.props vs Directory.Build.targets

文档中经常提到的另一个文件是 Directory.Build.targets。简而言之:

本文主要关注 props,但当你想要集中化“构建后执行此操作”的逻辑时,请记住 Directory.Build.targets

总结

Directory.Build.props 是那些默默解决大量痛点的功能之一:

一旦你采用了它,添加新项目就变得几乎微不足道——不再需要从某个“模板”项目复制设置,也不会忘记开启可空引用类型或分析器。

如果你今天已经在与一个庞大的解决方案作斗争:

  1. 在根目录添加一个 Directory.Build.props
  2. TargetFrameworkNullableImplicitUsings 和分析器设置移动到其中。
  3. 添加一个 tests/Directory.Build.props 用于通用的测试包。
  4. 清理你的 .csproj 文件,直到它们变得“无聊”为止。

未来的你(以及你的队友)会感谢你的。


标签


Previous Post
DbContext 非线程安全:正确并行化 EF Core 查询的方法
Next Post
.NET 10 网络功能改进深度解析