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.
- 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.
1 2 3 4 |
from a in Assemblies select new { a, AR= a.AssemblyReferences.Count().ToString() + ": " + a.AssemblyReferences.Select(ar => ar.ToString()).Aggregate(", ") } |
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.
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
.
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 theDirectory.Packages.props
file.PackageReference
with noVersion
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 theDirectory.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.