Best Practices for Versioning NuGet Packages in the Enterprise
by Eric Seng, on Sep 29, 2021 4:44:55 AM
Improperly versioning your NuGet packages can lead to lots of problems. Usually, it’s just headaches and wasted time from confusing and “broken” packages. But sometimes, this will lead to production problems like application crashes due to assemblies that didn’t load at runtime.
Versioning seems so simple – it’s just a number! But with NuGet, it’s anything but that. There are five distinct, multi-part version numbers that can be in a package, and each of these has its own formatting rules and behaviors.
In this article, I’ll explain how you can properly use version numbers to speed up the development process, keep things organized, avoid future headaches, and ensure your NuGet packages and applications are deployed seamlessly.
How are Version Numbers used in NuGet Packages?
Like with all software, developers use NuGet’s version numbers to identify one package from another. What makes v5.3.2 and v5.4.0 different -- aside from the number -- are the enhancements and bug fixes made between those versions.
Without version numbers, developers would struggle to keep track of which changes were made in which version, and eventually the wrong version would get deployed to production. Of course, this is true of any software: version numbers are an important communication tool between developers, stakeholders, and other humans.
However, version numbers are also used by NuGet/.NET in a few different ways.
#1. Manage Package Dependencies
NuGet Packages rarely function as a standalone unit. Most packages require at least one other package (called a “dependency”) to be installed before use. Dependencies often have dependencies, which means installing a single package can result in a “dependency tree” of required packages.
Dependency resolution is where things can get complex. Essentially, only one version of a NuGet package can be used at a time. If two different packages (let’s say A-5.4.2 and B-3.0.1) require different versions of another package (C-1.2.1 and C-1.0.3), then NuGet won’t be to resolve the dependencies. This means your project won’t build unless you pick different NuGet packages.
Fortunately, NuGet lets package authors use version ranges when specifying dependent packages. These can also get quite complex, but they essentially allow multiple package versions in different packages.
Following the example above:
<!— Package A can specify that C-1.2.1 to C-2.0.0 is okay -->
<PackageReference Include="Package.C" Version="[1.2.1,2.0.0)" />
<!—Package B can specify that any 1.x.y is okay -->
<PackageReference Include="Package.C" Version="1.*" />
As a result, NuGet would likely choose C-1.2.1 because it satisfies Package A and Package B’s dependency requirements.
#2. Loading Libraries Within Your Package
NuGet packages contain .NET library files (assemblies) that are loaded by your application on demand, at runtime. When your application loads these assemblies, it looks for a specific Assembly Version that’s embedded within the assembly’s DLL file. If the assembly version doesn’t match what your application expects, then it will crash with an assembly load error.
The assembly version number is totally separate from the package version number, and even has a different format. This may seem unintuitive at first, but keep in mind that .NET was created ten years before NuGet. Before then, assembly versions were the only way for applications to be tied to a specific version of a library.
Fortunately, you can override assembly version resolution using binding redirects, and NuGet automatically does this for you in most cases:
<!— Your application can specify that C-188.8.131.52 to C-184.108.40.206 is okay -->
<bindingRedirect oldVersion="220.127.116.11-18.104.22.168" newVersion="22.214.171.124"/>
But like with version ranges, binding redirects can get complicated, and will cause additional problems if not done right. These problems likely won’t be discovered until runtime.
#3. Displaying Important Information
Assembly DLL files may also embed two other version numbers: Assembly File Version and Assembly Informational Version. These version numbers are distinct from the Assembly Version that’s used for loading the assembly at runtime.
They’re purely informational, and can be found on the Details tab in Windows explorer:
The biggest problems these can cause are confusion when they don’t match the assembly version.
Managing the Five Version Numbers
The following table lists the different version numbers, their format, and how they’re used.
Comparison of Version Numbers in NuGet
|Package||SemVer (e.g. 1.0.3)
4-part Number (e.g. 126.96.36.199)
|Used to identify one version of a package from another|
|Dependency||Interval Notation (e.g. 1.2.1,2.0.0)
Floating Notation (e.g. 1.*)
|Used during build time to resolve dependencies and find needed packages|
|Assembly||4-Part Number (e.g. 188.8.131.52)||Used by .NET to load the assembly DLL at runtime|
|Assembly (File)||Free Text (4-Part Recommended)||Displayed on the version tab of the Windows file properties dialog.|
|Assembly (Informational)||Free Text||Used for informational purposes in some WinForms libraries|
Best Practice: Package Semantic Versioning
Although 4-part numbers are supported, Microsoft no longer recommends this format.
Instead, Semantic Versioning (SemVer) is preferred, and for good reason:
- The “SemVer2 spec” is very detailed and clearly defines what each of the 3-parts mean, leaving little room for deviation across libraries.
- The rules of semantic versioning are designed to not only help with dependency resolution, but with prerelease packages.
- Developers and business stakeholders need no training, and can understand basic SemVer numbers with minimal effort.
Semantic Versioning works by structuring each version identifier into three parts, MAJOR, MINOR, and PATCH. Each of these parts is managed as a number and incremented according to the following rules:
- Major releases (2.0.0) indicate changes that will be incompatible with previous versions.
- Minor releases (2.1.0) add functionality while still being backwards-compatible (in this example 2.1.0 will be compatible with 2.0.0).
- Patch releases are minor bug fixes or security patches that should always be fully backwards compatible (2.1.4).
Simple SemVer: Everything is a Major Release
Unlike the popular packages on nuget.org with millions of downloads, your NuGet packages may only be used by a handful of applications that you maintain. When you make a change to your library (even if it’s bug fix), you can just upgrade all the applications to use that version.
In cases like this, SemVer isn’t all that helpful, and may seem like overkill. But you should still use it, with one simple modification: every new version is a major version.
Instead of laboring whether ever set of changes would be a “Minor” or “Patch” version, just increment the “Major” version every time: 1.0.0, 2.0.0, 3.0.0 … 540.0.0, and so on. If you change your mind later, you can always introduce “Minor” and “Patch” versioning later.
Best Practice: Fixed Assembly Versions
In the pre-NuGet days, Assembly version numbers were important: libraries needed to be installed in the server’s Global Assembly Cache before .NET applications could use them. But these days, NuGet just copies the assembly DLL files next to your application’s executable files, and the whole application (libraries and all) are deployed to a server. This makes the assembly versions a relic of the past.
For most .NET applications, having your library assemblies use a “fixed” version number (like 184.108.40.206) means that you’ll never run into assembly load errors at runtime.
// Fixed version number
This also means that NuGet won’t need to overwrite your configuration files to add binding redirects. Which, if you think about it, is effectively doing the same thing: binding redirects essentially tell .NET to ignore the actual version number, anyways.
Alternative: Match Assembly Version and Package Version
For some libraries, using a fixed version number on your library may not be practical. In this case, you should use the same major, minor, and patch number as your package’s semantic version number, and use “0” as the fourth part:
// Package Version number 5.4.2
Best Practice: File & Informational Versions
When using a fixed Assembly Version number, you should still embed the version number in the DLL using the Assembly File Version attribute. This will make it easier to debug in the off chance you ever need to verify the right library was deployed with your application.
// Package Version number 5.4.2
You should avoid using Assembly Informational Version; it’s only displayed in a few places in Windows and is only used if the Assembly File Version is not present.
Best Practice: Don’t Deploy Prerelease Packages
A key part of the SemVer standard, prerelease labels let developers know if a NuGet package is still in development.
It’s fairly simple: a package version with a dash and descriptor after the patch number (e.g., 5.4.2-beta) is considered a prerelease package. Packages without those prerelease labels are considered “stable”.
To help prevent untested library code sneaking into production deployments, an easy rule to follow is to not allow packages with prerelease labels to be deployed as part of a production application.
Best Practice: Build Prerelease Packages
When you build a NuGet package – either manually using a tool like NuGet Package Explorer, or as part of a [NuGet CI/CD process] – your packages should always have a prerelease label. This clearly signifies that the library within the package has not been tested yet, and should not be used in production deployments.
Packages must have unique version numbers, and prerelease labels with “dot identifiers” are a simple way to manage this. There is no standard for what these identifiers should be, and it may take some trial-and-error to find what works for your team. One popular approach is to use “ci” along with a sequence number or datetime string.
- 4.2-ci.4 uses “4” as a sequence number to identify it’s the 4th build for 5.4.2
- 4.2-ci.202103041103 uses a long Datetime string for when the package was created
Best Practice: Repackage After Testing
If you’re never shipping prerelease NuGet packages to production, and you’re always building prerelease NuGet packages, then you’ll need to use a repackaging process that turns prerelease packages into stable ones.
Repackaging creates a new package from a prerelease package, using exactly the same content but changing the version number name. Because NuGet packages are just zip files, you can repackage it by:
- Downloading the package
- Opening the zip file
- Editing the .nuspec manifest file with the new version
- Saving the zip file
- Publishing the new package
By changing the manifest file, you have created a new package, identified by a different version number.
Repackaging ensures that what goes to production is exactly what you have tested: Your build server creates 5.4.2-ci, you test it, and upon approval, you repackage it as stable version 5.4.2.
Beyond Versioning and NuGet
Learning how to properly version your NuGet packages will reduce a lot of headaches and wasted time, and may even save you from production problems like application crashes.
But this is just the tip of the iceberg when using NuGet in an Enterprise, and we’re working on a free guide that covers this topic and much more!