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
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.
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, andAspire.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: setsRestorePackagesWithLockFile. It carries no versions.What to adopt
Central Package Management (NuGet 6.2+): a
Directory.Packages.propswithafter which every
.csprojcarries<PackageReference Include="X" />with noVersionattribute 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 withdotnet restore --force-evaluate, and if everypackages.lock.jsoncomes 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.propsexists withManagePackageVersionsCentrally, carrying every version currently in a.csproj.csprojcontains aVersion=attribute on aPackageReferencepackages.lock.jsonis 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 build0 warnings / 0 errors,dotnet testno regression against the then-current baseline10.*,1.*,3.*) preserved exactly as they are — this change moves versions, it does not re-decide themNotes for whoever picks this up
RestorePackagesWithLockFile; CPM and lock files are complementary, not alternatives.Directory.Packages.propsalso supportsGlobalPackageReferencefor 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.Follows: #683.