With .NET 8 almost released it is time to make upgrading easier. In this blog I’ll explain about Directory.Build.props and Directory.Packages.props and how these can make upgrading easier.

Most solutions consist of multiple projects. Even if all of your production code is in a single project chances are that your tests are in a different project. The separation that projects bring can be great, but sometimes managing dependencies can be a hassle.

The challenge with multiple csproj files

In many cases a solutions consists of multiple projects like depicted below.

└── solution/
    ├── src/
    │   ├── Main
    │   └── TheOtherProject
    └── tests/
        ├── IntegrationTests
        └── UnitTests

A typical csproj file contains a target framework, some project properties and some package references.

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

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
        <RootNamespace>Main</RootNamespace>
    </PropertyGroup>

  <ItemGroup>
      <PackageReference Include="Serilog" Version="3.0.1" />
      <PackageReference Include="Serilog.Extensions.Logging" Version="7.0.0" />
      <PackageReference Include="Serilog.Settings.Configuration" Version="7.0.1" />
      <PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
      <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\TheOtherProject.csproj" />
  </ItemGroup>
</Project>

Each project defines it’s own dependencies and the versions of those dependencies. What happens when TheOtherProject also uses Serilog but it is still on version 2.12.0? Most likely you’ll want all of those versions to be the same. Even with a good IDE like Rider and Visual Studio this can be a bit of a hassle.

A very similar problem migh arise when selecting the target framework. Chances are that you will have keep all of your projects within a solution aligned.

Creating clean and consistent project files

With .NET 6 Directory.Build.props and Directory.Packages.props were introduced. The names almost speak for themselves. These files contain directory wide build and packages properties. These files can be created in the top level of your solution folder. Typically placed where the sln file is stored.

Things like TargetFramework and ImplicitUSings can be set in the Directory.Build.props as below.

<Project>
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

Directory.Packages.props is straight forward as well:

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Serilog" Version="3.0.1" />
    <PackageVersion Include="Serilog.Extensions.Logging" Version="7.0.0" />
    <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" />
    <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
    <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
  </ItemGroup>
</Project>

With these 2 files created Main.csproj can be cleaned up:

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

    <PropertyGroup>
        <RootNamespace>Main</RootNamespace>
    </PropertyGroup>

  <ItemGroup>
      <PackageReference Include="Serilog" />
      <PackageReference Include="Serilog.Extensions.Logging" />
      <PackageReference Include="Serilog.Settings.Configuration" />
      <PackageReference Include="Serilog.Sinks.Console" />
      <PackageReference Include="Serilog.Sinks.File" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\TheOtherProject.csproj" />
  </ItemGroup>
</Project>

As can be seen the project file is now clear from any specific package or framework versions. These now depend on what is in the solution wide properties. That will aso make any package updates and framework updates consistent, by definition.

Installing packages

Support for these property files is fully embedded in the dotnet ecosystem. For example, when running dotnet add package Serilog a terminal under solution/src/TheOtherProject/. Only “<PackageReference Include="Serilog" />” is added to TheOtherProject.csproj. Notice hte absence of a version number because it relies on what is in Directory.Packages.props already.

The same goes for added a packages that no other project hase reference yet. donet add will update TheOtherProject.csproj and in addition update Directory.Packages.props with the selected version.

Conclusion

Use of Directory.Build.props and Directory.Packages.props make a solution just a bit easier toe maintain. This will make an upgrade from .NET 6 or 7 to .NET 8 less complicated. The best is that you can do this before .NET 8 is out and already enjoy better package versioning.

In my experience bot Rider and Visual Studio also deal with this solution wide properties well. But always check your commits as from now on a packages version in a csproj file is something to try and avoid.