Documentation

adrs/050-central-package-management.md

ADR-050: Central Package Management with Directory.Packages.props

Status

Accepted

Context

The Acsis Core repository contains 50+ projects across components, bases, projects, and development resources. Managing NuGet package versions across this many projects presents significant challenges:

Problems with Decentralized Package Management

  1. Version Inconsistency: Different projects could reference different versions of the same package, leading to runtime conflicts and debugging nightmares
  2. Upgrade Burden: Updating a commonly-used package (e.g., Microsoft.EntityFrameworkCore) required editing dozens of project files
  3. Merge Conflicts: Package version changes in .csproj files caused frequent merge conflicts
  4. Audit Difficulty: No single place to see all packages and versions used in the solution
  5. Dependency Hell: Transitive dependency conflicts were difficult to diagnose when versions varied
  6. Security Updates: Applying security patches required finding and updating every project using the vulnerable package

Industry Context

NuGet introduced Central Package Management (CPM) in .NET 6 as the recommended approach for managing package versions in multi-project solutions. This feature allows defining package versions in a single Directory.Packages.props file that applies to all projects in the directory tree.

Decision

We use Central Package Management via Directory.Packages.props at the repository root to manage all NuGet package versions.

Implementation

Directory.Packages.props Structure:

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <!-- Packages listed alphabetically -->
    <PackageVersion Include="Aspire.Hosting" Version="13.1.0" />
    <PackageVersion Include="AutoMapper" Version="14.0.0" />
    <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
    <!-- ... all other packages -->
  </ItemGroup>
</Project>

Project File Usage:

<!-- In any .csproj file -->
<ItemGroup>
  <!-- Version is NOT specified - comes from Directory.Packages.props -->
  <PackageReference Include="Microsoft.EntityFrameworkCore" />
  <PackageReference Include="AutoMapper" />
</ItemGroup>

Conventions

  1. Alphabetical Ordering: Packages are listed alphabetically to reduce merge conflicts and improve discoverability
  2. Comments for Context: Non-obvious packages include brief comments explaining their purpose
  3. Grouped by Category: Related packages (e.g., all Aspire packages) are kept together with comments
  4. Single Source of Truth: All package versions MUST be defined in Directory.Packages.props
  5. No Version Overrides: Projects should not override versions unless absolutely necessary (and documented)

Package Organization Example

<ItemGroup>
  <!-- Aspire orchestration packages -->
  <PackageVersion Include="Aspire.Hosting" Version="13.1.0" />
  <PackageVersion Include="Aspire.Hosting.AppHost" Version="13.1.0" />
  <PackageVersion Include="Aspire.Npgsql.EntityFrameworkCore.PostgreSQL" Version="13.1.0" />

  <!-- Model mapping -->
  <PackageVersion Include="AutoMapper" Version="14.0.0" />
  <PackageVersion Include="Riok.Mapperly" Version="4.3.0" />

  <!-- Testing -->
  <PackageVersion Include="Shouldly" Version="4.3.0" />
  <PackageVersion Include="xunit" Version="2.9.3" />
  <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

Consequences

Positive

  1. Single Source of Truth: One file contains all package versions for the entire repository
  2. Consistent Versions: All projects automatically use the same package versions
  3. Easy Upgrades: Update a package version once, applies everywhere
  4. Reduced Merge Conflicts: Fewer files change during package updates
  5. Better Auditing: Easy to review all dependencies for security vulnerabilities
  6. Smaller Project Files: .csproj files are cleaner without version specifications
  7. Transitive Consistency: Reduces diamond dependency problems
  8. IDE Support: Visual Studio and Rider fully support CPM

Negative

  1. Learning Curve: Developers must understand CPM if coming from traditional NuGet usage
  2. Tooling Requirements: Some older NuGet tools may not support CPM
  3. Version Override Friction: When a project genuinely needs a different version, requires explicit override with justification
  4. Single File Bottleneck: High-traffic file during package update periods

Mitigation Strategies

For Learning Curve:

  • Document the pattern in developer onboarding
  • Include comments in Directory.Packages.props explaining usage

For Version Overrides (when necessary):

<!-- In project file - ONLY when justified -->
<PackageReference Include="SomePackage" VersionOverride="1.2.3" />
<!-- Must include comment explaining why override is needed -->

For Merge Conflicts:

  • Alphabetical ordering minimizes conflicts
  • Use short-lived branches for package updates
  • Consider dedicated package update PRs

Adding New Packages

  1. Add <PackageVersion Include="PackageName" Version="X.Y.Z" /> to Directory.Packages.props
  2. Maintain alphabetical order
  3. Add comment if package purpose isn't obvious
  4. In project file, add <PackageReference Include="PackageName" /> (no version)

Updating Package Versions

  1. Edit version in Directory.Packages.props
  2. Build solution to verify compatibility
  3. Run tests to catch breaking changes
  4. Single commit updates all projects
  • ADR-006: Centralized MSBuild Configuration (companion pattern)
  • ADR-029: .NET 10 LTS Migration (major package version updates)

References