“It worked when I built it” or “It works on my machine”. These are typically followed by frustrated moans and an investigation to identify the source of the problem. Unfortunately, NuGet packages and their dependencies are one common reason these situations occur.
Package dependencies are a lot more complex than they seem on the surface. If you’re not careful, NuGet can resolve them in a way that leads to unwanted packages being introduced into your code, perhaps even into production!
In this article I will:
- Explain how unwanted packages can sneak into your code.
- Describe how to use Lock Files to ensure that the packages you approved are the same ones being used in your application each time you build it.
- Describe how to use Package Consumers to quickly identify which applications are using a specific package so you can update them when necessary.
The Unintended Side Effects of Dependency Resolution
NuGet Packages almost always depend on other packages. This results in a dependency tree that must be resolved for your project to build.
As you can see from the diagram above, this can quickly become difficult, especially considering that even the wrong version of the correct package will prevent dependency resolution. This is typically solved by using version ranges.
Version ranges are used to specify a range of versions your package will accept. This is extremely helpful when resolving dependency trees, but it has some unintended consequences.
Let’s say you are using the package Oracle.ManagedDataAcess.Core 3.21.4. This package is dependent on the System.DirectoryServices.Protocols package. To resolve this dependency, a version range is used to tell Oracle.ManagedDataAcess.Core to accept any version of System.DirectoryServices.Protocols that is greater than or equal to version 5.0.0
Pretty convenient right? Yes, it is. But it comes at a cost!
The image above contains a list of version updates for the package System.DirectoryServices.Protocols. Version 5.0.1 was released 7 days ago (as of the time of writing) and version 6.0 is due to replace it soon. Since the version range for Oracle.ManagedDataAcess.Core was set to accept any version of 5.0.0 or higher, this means that your application would automatically include a new package – perhaps, an unwanted one.
The unexpected acceptance of unapproved third-party packages causes two common issues:
- Unpredictable builds that lead to “it worked when I built it yesterday” situations.
- Unsafe packages sneaking into your code.
Version ranges make solving dependencies much easier, but the issues that come with them need to be mitigated. Fortunately, this can be accomplished using lock files.
Best Practice: Use Lock Files for Repeatable Builds
Lock files “lock” all versions for the entire dependency tree at the time that the lock file is created and shows you every package dependency being used by an application.
Why is this so helpful?
The NuGet client resolves the most current version of a package at build-time, during the dependencies install of a package. This could have been different than the versions that were originally intended at development time. If you don’t “lock” the package versions when you develop an application, your build may change the next time you or someone on your team builds the application.
To enable the use of lock file with NuGet, set the MSBuild property RestorePackagesWithLockFile in your project file:
If this property is set, NuGet restore will generate a lock file – packages.lock.json file at the project root directory. Besides locking in the package versions, the lock file will also show all the packages your application depends on.
Lock files solve the massive headache of unpredictable package versions sneaking into production… but they don’t eliminate the problems with dependencies completely.
Watch out for New Dependencies
Keep in mind that dependencies can – and often will – have dependencies. And these dependencies can change, which means you may find yourself with different packages altogether when you update. And these packages can have a license, vulnerability, and quality risks.
The best way to mitigate this risk is by setting up a package approval workflow that only allows approved packages to be used by developers and build servers.
Watch out for New Vulnerabilities
Even after you’ve started utilizing lock files and have a workflow that makes sure you’re only using approved packages – it’s entirely possible that a new vulnerability will be discovered in a dependency.
This vulnerability could then impact any applications that use it. So what can you do?
You could manually inspect the lock files of all your applications to see which ones are using the unwanted dependency. That’s a lot of repositories to check out and a lot of searching.
But you can also use ProGet’s Package Consumer Feature to do this automatically.
Best Practice: Use Package Consumers to Track Dependencies
ProGet’s Package Consumers feature shows all the applications that are “consuming” or using a specific package. So let’s say the package dependency in question is System.DirectoryServices.Protocols 5.0.0.
After building your application, ProGet’s Package Consumers feature uses pgscan to scan the build output, search for the specific package versions consumed by the application and publish that data to ProGet along with your application’s name and version.
Using Package Consumer we can see that the package System.DirectoryServices.Protocols 5.0.0. is being used in applications ThatOtherAPP, MyApp, & OtherApp. Now you know exactly which applications will be affected and can quickly make the relevant changes!
By the way… this isn’t just an example for theoretical purposes.
System.DirectoryServices.Protocols has been downloaded 17.6M times and almost all of them were before the recently discovered vulnerability:
Anyone that has an application using a package that depends on System.DirectoryServices.Protocols now suddenly have a vulnerable package in their code. Anyone that finds themselves in this situation will definitely want to know which applications are using this package as quickly as possible. This is exactly why we created package consumers 😀.
NuGet in the Enterprise
Utilizing lock files, a package approval workflow, and ProGet’s package consumer will help ensure predictable builds and keep unwanted packages out of them, but there is still a lot more to learn if you want to effectively use NuGet in the enterprise.
Check out our free guide to learn more!