Skip to content

chore: adopt Central Package Management so a version bump is one file, not eight #684

Description

@mforce

Amendment (2026-09-04): shipped in #689. The byte-identical lock-file criterion below was replaced by a per-entry resolved-graph proof; see #684 (comment) for what changed and why.

The problem

NuGet versions live scattered across every .csproj. Bumping the Aspire packages in #683 meant editing three separate project files, and Aspire.Hosting.* 13.5.1 appears in two of them independently — nothing makes them move together, and nothing fails if they drift apart.

We already have Directory.Build.props, but it does exactly one thing: sets RestorePackagesWithLockFile. It carries no versions.

What to adopt

Central Package Management (NuGet 6.2+): a Directory.Packages.props with

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Serilog.AspNetCore" Version="10.*" />
    <!-- … one line per package … -->
  </ItemGroup>
</Project>

after which every .csproj carries <PackageReference Include="X" /> with no Version attribute at all. One place to bump, and two projects cannot silently disagree about the same package.

Why this is worth doing as its OWN PR — and the reason is a verification property

CPM adoption is provably behaviour-neutral, and the lock files are the proof. Move every version into Directory.Packages.props, regenerate with dotnet restore --force-evaluate, and if every packages.lock.json comes out byte-identical, the refactor demonstrably resolved to the same graph. A refactor with a mechanical proof of no-op is rare and worth preserving.

That proof is destroyed if it rides along with a version bump, because the locks have to change and you can no longer attribute a change to one or the other. Hence: land #683 first, then this on top of it.

Acceptance

  • Directory.Packages.props exists with ManagePackageVersionsCentrally, carrying every version currently in a .csproj
  • No .csproj contains a Version= attribute on a PackageReference
  • Every packages.lock.json is byte-identical to its pre-change content — git diff --stat -- '**/packages.lock.json' is empty. This is the acceptance criterion, not a nice-to-have: a non-empty diff means the refactor changed resolution and needs explaining before it merges.
  • dotnet build 0 warnings / 0 errors, dotnet test no regression against the then-current baseline
  • Floating ranges (10.*, 1.*, 3.*) preserved exactly as they are — this change moves versions, it does not re-decide them

Notes for whoever picks this up

  • Interacts cleanly with RestorePackagesWithLockFile; CPM and lock files are complementary, not alternatives.
  • Directory.Packages.props also supports GlobalPackageReference for things every project needs — worth considering separately, but not in this PR: that changes what projects reference, which is a behaviour change and would break the byte-identical-lock proof.
  • Watch for any project deliberately pinning a different version of the same package than its siblings. If one exists, CPM forces the question — resolve it explicitly and say so, rather than silently unifying.

Follows: #683.

Activity

  1. mforce commented on Sep 4, 2026

    @mforce
    OwnerAuthor

    Amendment (2026-09-04, PR #689). The acceptance criterion "git diff --stat -- '**/packages.lock.json' is empty" cannot be met under Central Package Management: enabling ManagePackageVersionsCentrally makes NuGet write lock format "version": 2 and relabel transitive packages that carry a PackageVersion entry as CentralTransitive, in every project, regardless of whether resolution changed.

    Owner accepted the replacement proof in review: a per-entry comparison of all 443 lock entries across the nine projects, asserting the same package set per TFM and identical resolved, contentHash and dependencies for every entry, allowing only version 1→2 and Transitive→CentralTransitive. Script and result are in the #689 description. Every other criterion holds as written.

    Two requirements not in the issue that the PR had to add: CentralPackageFloatingVersionsEnabled=true (CPM rejects 10.* ranges with NU1011 otherwise), and naming Directory.Packages.props in every place that named the csproj or Directory.Build.props as a restore input (Dockerfile restore layer, the #315 drift guard, cache and path filters, pre-commit trigger).

  2. added a commit that references this issue on Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions