.NET 10 NU1015: Fix PackageReference Without Version Restore Failures

.NET 10 NU1015: Fix PackageReference Without Version Restore Failures

.NET 10 NU1015 turns a PackageReference without a version into a restore error. I like the stricter default because an unbounded direct dependency can quietly resolve the lowest package version. The catch is that versionless XML is also the correct shape for NuGet Central Package Management (CPM). A mechanical “add Version everywhere” repair can undo the policy your repository intended to enforce. I use a simple split: first decide who owns the version, then make restore prove the answer.

.NET 10 中的 NU1015 错误将缺少版本号的 PackageReference 视为还原(restore)错误。我喜欢这种更严格的默认设置,因为未绑定的直接依赖项可能会静默地解析为最低的包版本。但问题在于,不带版本的 XML 格式也是 NuGet 中央包管理(CPM)的正确配置方式。机械地在所有地方“添加版本号”可能会破坏你仓库原本想要强制执行的策略。我采用一种简单的拆分方法:首先确定谁拥有版本控制权,然后通过还原操作来验证结果。

Why .NET 10 NU1015 stops the build

为什么 .NET 10 的 NU1015 会停止构建

Before .NET 10, NuGet reported NU1604 when a direct reference had no inclusive lower bound. Restore could continue and select the lowest version available from the configured sources. Starting with .NET 10, the same mistake produces NU1015 and restore fails. Microsoft documents this as a stable behavioral change in the .NET 10 compatibility guidance.

在 .NET 10 之前,当直接引用没有包含下限时,NuGet 会报告 NU1604。还原过程可以继续,并从配置的源中选择可用的最低版本。从 .NET 10 开始,同样的错误会产生 NU1015,导致还原失败。微软在 .NET 10 兼容性指南中将此记录为一项稳定的行为变更。

Here is the ambiguous project entry: 这是一个模棱两可的项目条目:

<ItemGroup>
  <PackageReference Include="Demo.Greeting" />
</ItemGroup>

If this is a normal direct reference, the project is missing its version. If CPM is active, the project is correct and the version should live elsewhere. The NU1015 diagnostic reference calls out a common failure mode: a project that expected CPM was copied into a location where CPM is disabled or its props file is no longer discovered. That distinction matters more than silencing the error. It tells me whether the project file or the repository-level package policy is broken.

如果这是一个普通的直接引用,那么项目缺少版本号。如果启用了 CPM,那么项目配置是正确的,版本号应该存在于其他地方。NU1015 诊断参考指出了一种常见的故障模式:一个依赖 CPM 的项目被复制到了 CPM 被禁用或其 props 文件无法被发现的位置。这种区别比单纯消除错误更重要。它能告诉我到底是项目文件损坏了,还是仓库级别的包策略出了问题。

The timing can be misleading. An SDK upgrade may expose an old direct reference that had always relied on lowest-version resolution, while a repository move may break a previously valid CPM import. I inspect the failing project’s evaluated inputs, nearby props files, and recent path changes before editing package metadata. That keeps a restore migration from turning into an accidental package-management migration.

时间点可能会产生误导。SDK 升级可能会暴露一个一直依赖“最低版本解析”的旧直接引用,而仓库迁移可能会破坏之前有效的 CPM 导入。在编辑包元数据之前,我会检查失败项目的评估输入、附近的 props 文件以及最近的路径更改。这可以防止将还原迁移变成意外的包管理迁移。

Fix the owner, not only the XML

修复所有者,而不仅仅是 XML

For a direct reference, I add an explicit version: 对于直接引用,我添加一个显式版本:

<PackageReference Include="Demo.Greeting" Version="2.0.0" />

For CPM, I keep the project reference versionless and put the version in the nearest Directory.Packages.props: 对于 CPM,我保持项目引用不带版本,并将版本放在最近的 Directory.Packages.props 中:

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Demo.Greeting" Version="2.0.0" />
  </ItemGroup>
</Project>

NuGet automatically imports only the first Directory.Packages.props it finds while walking up from a project. A nested file can therefore change which policy applies, and a copied project can lose the import completely. The official Central Package Management guide describes both the enabling property and the nearest-file rule. I check the evaluated project context instead of assuming that a props file somewhere in the repository is active. The two valid outcomes are clear: a direct PackageReference owns its Version, or an active CPM file owns the matching PackageVersion.

NuGet 在从项目向上查找时,只会自动导入它找到的第一个 Directory.Packages.props。因此,嵌套文件可能会改变应用的策略,而复制的项目可能会完全丢失导入。官方的中央包管理指南描述了启用属性和“最近文件”规则。我倾向于检查评估后的项目上下文,而不是假设仓库中某个地方的 props 文件处于活动状态。两种有效的结果很明确:要么直接的 PackageReference 拥有其 Version,要么活动的 CPM 文件拥有匹配的 PackageVersion

Reproduce NU1015 without an external feed

在没有外部源的情况下重现 NU1015

The runnable sample creates a local package feed containing Demo.Greeting versions 1.0.0 and 2.0.0. It then tests three consumers: BrokenDirect has no version and must fail with NU1015. FixedDirect pins 2.0.0 on the reference. CentralManaged leaves the reference versionless and pins 2.0.0 in Directory.Packages.props.

可运行的示例创建了一个包含 Demo.Greeting 1.0.0 和 2.0.0 版本的本地包源。然后它测试了三个消费者:BrokenDirect 没有版本号,必须以 NU1015 失败;FixedDirect 在引用中固定了 2.0.0 版本;CentralManaged 让引用保持无版本,并在 Directory.Packages.props 中固定了 2.0.0 版本。

From the sample folder, I run: 在示例文件夹中,我运行:

dotnet restore .\Verifier\Verifier.csproj
dotnet build .\Verifier\Verifier.csproj -c Release --no-restore
dotnet run --project .\Verifier\Verifier.csproj -c Release --no-build --no-restore

The verifier packs both fixture versions, runs each restore, reads project.assets.json, and executes the repaired projects. The important assertions are not just “restore passed.” Both supported repairs must resolve and run with 2.0.0. The sample also demonstrates the documented compatibility switch: 验证器打包了两个测试版本,运行每次还原,读取 project.assets.json,并执行修复后的项目。重要的断言不仅仅是“还原通过”。两种支持的修复方案都必须解析并运行 2.0.0 版本。该示例还演示了文档中提到的兼容性开关:

dotnet restore .\BrokenDirect\BrokenDirect.csproj `
  -p:SdkAnalysisLevel=9.0.300 `
  -p:TreatWarningsAsErrors=false

That brings back NU1604, and the local feed proves why I do not treat it as the fix: NuGet chooses 1.0.0, the lowest available version. The complete validation and review history is in the merged pull request.

这会带回 NU1604,而本地源证明了为什么我不将其视为修复方案:NuGet 会选择 1.0.0,即最低可用版本。完整的验证和审查历史记录在合并的拉取请求中。

Limits and when I avoid the escape hatch

限制以及我何时避免使用“逃生舱”

SdkAnalysisLevel=9.0.300 affects every SDK behavior gated by that level, not only this diagnostic. A warnings-as-errors policy can also promote NU1604 back to a failure. I reserve the switch for short migration diagnostics while I determine who should own the version.

SdkAnalysisLevel=9.0.300 会影响该级别控制的所有 SDK 行为,而不仅仅是此诊断。将警告视为错误(warnings-as-errors)的策略也可能将 NU1604 重新提升为失败。我仅在确定谁应该拥有版本控制权期间,将此开关用于短期的迁移诊断。

The local-feed sample does not model authenticated sources, source mapping, package lock files, or feed outages. Those controls still matter in a production restore. Its narrower job is to make the ownership decision and resolved version deterministic. I also would not add Version="0.0.0" merely to make NU1015 disappear. Microsoft documents that value for the unusual case where the lowest version is genuinely intended, but it still produces NU1603 when NuGet resolves a higher available version.

本地源示例没有模拟身份验证源、源映射、包锁定文件或源中断。这些控制在生产环境的还原中仍然很重要。它的狭义任务是使所有权决策和解析版本具有确定性。我也不会仅仅为了让 NU1015 消失而添加 Version="0.0.0"。微软记录了该值用于确实需要最低版本的特殊情况,但当 NuGet 解析到更高的可用版本时,它仍然会产生 NU1603。

Most application dependencies need a deliberate version or an active central policy instead. For direct references, I pin the intended dependency. For CPM repositories, I verify the import boundary and keep versions centralized. Either way, I leave restore with one explicit owner and a test that checks the resolved graph.

大多数应用程序依赖项需要一个明确的版本或活动的中央策略。对于直接引用,我固定预期的依赖项。对于 CPM 仓库,我验证导入边界并保持版本集中化。无论哪种方式,我都会确保还原过程有一个明确的所有者,并进行测试以检查解析后的依赖图。

Have you hit NU1015 because a direct version was missing, or because a project silently lost its CPM context? Happy coding! 你是因为缺少直接版本号,还是因为项目静默丢失了 CPM 上下文而遇到 NU1015?祝编码愉快!