DEV Community

Sukhpinder Singh
Sukhpinder Singh

Posted on

.NET 10 NU1510 Package Pruning: Fix CI Without Breaking Legacy Targets

.NET 10 NU1510 package pruning can turn an SDK upgrade into a failed restore when a repository promotes warnings to errors. The tempting fix is to delete the named PackageReference everywhere.

That is safe for a net10.0-only application when the framework supplies the assembly, but it can break a library that still targets netstandard2.0. I treat NU1510 as a target-specific dependency decision: prove where the reference is redundant, retain it where it is required, and verify the package produced for consumers.

Why NU1510 becomes a CI failure

Beginning with .NET 10, package pruning is enabled by default for projects targeting .NET 10 or later. NuGet raises NU1510 when a direct reference to a package registered for pruning can be completely removed because the targeted SDK supplies the same or a higher assembly version.

The NU1510 diagnostic reference is precise about that boundary. This is not a general-purpose unused-package detector, and it does not apply automatically to arbitrary third-party packages.

The sample applies a CI-style policy globally:

<Project>
  <PropertyGroup>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

Its intentionally broken net10.0 project then adds this direct reference:

<ItemGroup>
  <PackageReference Include="System.Text.Json" Version="10.0.11" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

With the stable .NET SDK 10.0.303 used for the sample, restore exits with code 1 because NU1510 is promoted from a warning to an error. Without a warnings-as-errors policy, the same condition is normally a warning rather than a failed restore.

Microsoft's .NET 10 breaking-change guidance recommends removing the reference when every target can prune it, or conditioning it when an older target still needs it.

Fix .NET 10 NU1510 package pruning per target

For the net10.0-only application, the fix is simply to remove the package reference:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
</Project>
Enter fullscreen mode Exit fullscreen mode

The application still imports System.Text.Json and serializes the same payload. Its verified output is:

{"Message":"framework-provided","Count":10}
Enter fullscreen mode Exit fullscreen mode

A multi-target library needs a different answer. The sample supports netstandard2.0 and net10.0, so deleting the reference globally would remove a dependency required by the older target. I make that requirement explicit in MSBuild:

<PropertyGroup>
  <TargetFrameworks>netstandard2.0;net10.0</TargetFrameworks>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
  <PackageReference Include="System.Text.Json" Version="10.0.11" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

An unconditioned reference that remains necessary for one runtime target does not produce NU1510, because it cannot be removed from every target. Current pack behavior also omits a prunable dependency from the net10.0 group while retaining it for the older target. I use the explicit condition to make the intent reviewable, not because every unconditioned multi-target reference is wrong.

NuGet restore creates a separate dependency graph for each target framework, and dotnet pack produces framework-specific dependency metadata. The PackageReference documentation documents target conditions. For a larger framework matrix, a compatibility-based condition may be easier to maintain, but it still needs verification against every supported target.

Verify the dependency graph, not only the warning

I do not consider a disappearing warning sufficient evidence. A library fix must also preserve the dependency contract seen by package consumers.

The sample runs its checks with:

dotnet restore .\Verifier\Verifier.csproj
dotnet build .\Verifier\Verifier.csproj --configuration Release --no-restore
dotnet run --project .\Verifier\Verifier.csproj --configuration Release --no-build --no-restore
dotnet format whitespace .\Nu1510PackagePruning.slnx --verify-no-changes --no-restore
Enter fullscreen mode Exit fullscreen mode

The verifier owns the expected failure, so the broken project is deliberately excluded from the solution. It checks nine behaviors: the broken restore fails with NU1510; the diagnostic names System.Text.Json; the fixed application runs; the fixed assets contain no package copy; and the multi-target assets plus packed .nuspec keep the dependency only for netstandard2.0.

The completed verifier reports PASS 9/9. Five repeated runs also produced byte-identical output. You can inspect the complete runnable sample and the merged pull request.

This distinction matters: the sample executes the modern application, while the older library target is validated through restore, build, assets metadata, and the packed .nuspec. It does not claim to execute a netstandard2.0 application.

Limitations and when not to remove a reference

This approach applies only when the SDK or a referenced framework has registered a package for pruning. A custom package is not made redundant merely because its namespace resembles framework functionality.

The sample covers the SDK-provided System.Text.Json case. It does not model the separate FrameworkReference and transitive ProjectReference scenario described in the diagnostic documentation. It also makes no benchmark claim about restore size or speed.

Before removing a reference in a real library, I would restore, build, test, and pack every supported target. I would also inspect the resulting dependency groups, especially when central package management or more complex MSBuild conditions are involved. Suppressing NU1510 may unblock a migration temporarily, but it does not establish whether the published dependency contract is correct.

Which multi-targeted package reference will you audit before your next .NET 10 upgrade?

Happy coding!

Top comments (0)