NDepend Blog

Improve your .NET code quality with NDepend

Understand NuGet Central Package Management and Transitive Pinning with Examples

January 16, 2025 4 minutes read
Understand NuGet Central Package Management and Transitive Pinning with Examples
Nowadays, .NET  applications often rely on numerous NuGet packages, each of which may depend on additional packages. As a result, it’s common for multiple versions of the same package to be used within a solution. This can cause unexpected behavior and lead to complex bugs.

This post explores five versioning scenarios and introduces tools to detect and resolve them. This post may appear lengthy due to the numerous screenshots, but it should only take a few minutes to understand its points.

Scenario 1: Direct Reference of Different Versions

Let’s begin with a straightforward scenario: a solution contains a console project that references a library project. Each project uses a different version of the Newtonsoft.Json package.

After compiling the solution and generating a web report with NDepend (version 2025.1 to be released in March 2025), the following issues are identified.
  • The NDepend Software Composition Analysis rule identifies that the solution is using a version of Newtonsoft.Json with a known security advisory. A warning is also shown in the Visual Studio solution explorer for this security advisory:

  • Additionally, another rule flags that multiple versions of the Newtonsoft.Json package are being referenced.

This CQLinq query below lists assembly references for all application and third-party assemblies.

Here is the result obtained.

Finally by executing the console project we can see that the highest referenced version of Newtonsoft.Json is loaded at runtime.

Runtime1_

What can go wrong?

If the library project depends on public code elements exposed in Newtonsoft.Json v11.0 but removed in Newtonsoft.Json v13.0, a compile-time error is triggered because the compiler only considers Newtonsoft.Json v13.0.

However, if the console project is unloaded at compile time or if it is defined in another solution, Newtonsoft.Json v11.0 is resolved and the library project compiles. An exception might occur at runtime when running the console because of the missing elements in v13.0.

Scenario 2: Same as Scenario 1 + Transitive Reference of a Third Version

Now let’s consume the package NJsonSchema v10.7.1 from the library. This package consumes Newtonsoft.Json v9.0.0 .

Config2

The NDepend rule Assemblies References in Multiple Versions now mentions that 3 versions of Newtonsoft.Json are referenced from 3 assemblies. Rather than being taken from the web report (like in scenario 1 section), the screenshot below displays the NDepend code rule editor along with its result.

The code query used in the Scenario 1 also shows that 3 different versions are referenced. NJsonSchema is displayed in bold font because it wasn’t defined in Scenario 1, which serves as the baseline for NDepend.

Again at runtime, only Newtonsoft.Json v13.0 is loaded, presenting the same risks as previously mentioned.

Scenario 3: Same as Scenario 2 + ManagePackageVersionsCentrally

Let’s rewrite the Scenario 2 but this time with the property ManagePackageVersionsCentrally set to true in a Directory.Packages.props file stored in the root folder of the solution. Notice that:

  • PackageVersion is used in the Directory.Packages.props file.
  • PackageReference with no Version attribute is used in each project file consuming the package. The version resolved at compile time and loaded at runtime is the version mentioned in the Directory.Packages.props file.

Since package versions are now managed centrally, both the console and the library assemblies reference Newtonsoft.Json v13.0. Still NJsonSchema references Newtonsoft v9.0 and its code will have to deal with v13.0 at runtime.

Scenario 4: ManagePackageVersionsCentrally Doesn’t Help on Transitive Dependency

Now we comment the references to Newtonsoft.Json and the code relying on Newtonsoft.Json in both solution projects. Only NJsonSchema references Newtonsoft.Json. We say that Newtonsoft.Json is not anymore a direct dependency but it is a transitive dependency of our solution.

The code query highlights that only NJsonSchema consumes Newtonsoft.Json.

Despite having both <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> and <PackageVersion Include="Newtonsoft.Json" Version="13.0.0" /> in the Directory.Packages.props only Newtonsoft v9.0 is resolved at compile time…

… and at runtime.

Scenario 5: Using CentralPackageTransitivePinningEnabled to Pin the Version of Transitive Reference

Newtonsoft.Json v9.0 has some security advisories. In the Scenario 4 above we can reference a newer version of the package NJsonSchema. But if this newer version doesn’t reference a security advisory free version of Newtonsoft.Json we are stuck with an unsafe version!

Hopefully we can set the setting <CentralPackageTransitivePinningEnabled> to true in the Directory.Packages.props file.

This setting forces MSBuild to resolve the mentionned version of Newtonsoft.Json.

The assembly NJsonSchema still references Newtonsoft.Json v9.0.0 because it is not modified when compiling the solution. However at runtime Newtonsoft.Json v13.0 is loaded.

Conclusion

We hope you learned something from this post, as our experiment taught us a few interesting insights.

On one hand it may not be feasible for your application to reference only a single version of a package if you don’t have control over its transitive dependencies. Because only a single version of a package is loaded at runtime this situation might results in unexcepted bugs and behavior.

On the other hand, you can still enforce the use of a security advisory-free version of a package at runtime.

These versioning features will let your team better manage dependencies as your projects scale with the convenience of a centralized solution. Also Software Composition Analysis is now easier to achieve in .NET.

 

Leave a Reply

Your email address will not be published. Required fields are marked *