en

Central Package Management

Why PCPM leans on Microsoft's CPM, not against it.

Central Package Management is a feature of the .NET SDK that moves package versions out of individual .csproj files and into a single Directory.Packages.props at the root of the workspace. PCPM is built on top of CPM rather than against it; this page explains why and how to make the most of it.

The CPM file

A Directory.Packages.props is a normal MSBuild .props file. It sits in the workspace root and is auto-imported by every .csproj under the same directory tree. Its job is to declare <PackageVersion /> entries:

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Serilog" Version="3.1.1" />
    <PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
    <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>
</Project>

The <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> property is what makes this work — it tells MSBuild that the version for any <PackageReference /> should come from this file rather than the .csproj.

A <PackageReference /> in any .csproj then looks like:

<ItemGroup>
  <PackageReference Include="Serilog" />
</ItemGroup>

No Version attribute. The version is implicit; the .csproj only knows the id.

Why this is the right substrate for PCPM

The most common complaint about package managers is “version drift across projects.” With CPM, that complaint is structurally impossible: there is one place to put a version, and it applies everywhere. The fact that PCPM writes the version for you when you run pcpm add is a convenience, not a requirement — you can hand-edit Directory.Packages.props and pcpm install will pick up the change.

The second reason is that CPM is already a MSBuild feature. The .NET SDK, Visual Studio, Rider, and dotnet restore all understand it. PCPM doesn’t have to ship a build target, a custom resolver, or a NuGet.Protocol plugin. It just writes CPM files.

The third reason is that the .NET community has settled on CPM as the answer to the “many csproj” problem. By leaning on it, PCPM inherits the solution that’s already winning. By building against it, PCPM gets to focus on the parts that CPM doesn’t solve: the store, the hardlinks, the strict resolution.

The CPM-friendly file model

PCPM touches only three files in your workspace:

  • Directory.Packages.props — written by pcpm add, pcpm remove, pcpm convert. Never overwritten if it already exists; PCPM adopts existing entries.
  • pcpm.json — written by pcpm init. The PCPM manifest. You almost never edit this by hand.
  • pcpm.lock — written by pcpm install. The resolved graph with content hashes. Generated, never hand-edited.

The .csproj files are touched by pcpm add and pcpm remove to add or remove <PackageReference /> entries, and by pcpm convert to strip the Version attribute (in the case where you weren’t using CPM before).

The MSBuild .targets files PCPM ships (pcpm.MsBuild.targets) are pulled in automatically via the pcpm.MsBuild NuGet package. This package contains a RelinkBin task that re-hardlinks the output of dotnet build to the store, so the binlog on CI doesn’t grow with each build. The package is opt-in; install it once per workspace if you want this behavior.

Edge cases

  • Per-project version overrides. You can override the CPM version for a single project with <PackageReference Include="X" Version="1.2.3" OverrideCentralPackageVersions="true" />. PCPM respects this; it just adds the override to the resolution graph as a tighter constraint.
  • Per-TFM versions. <PackageVersion Include="X" Version="1.0.0"><Conditions><TargetFramework>net48</TargetFramework></Conditions></PackageVersion> is supported. PCPM doesn’t ship a UI for it; hand-edit the .props if you need it.
  • Private feeds. If you have a NuGet.config with <packageSource>, PCPM picks it up and writes the same feeds into pcpm.json.

See also