NuGet
Best Practices for Versioning NuGet Packages at Scale
This article is part of our series on NuGet at Scale, also available as a chapter in our free, downloadable eBook.
Everything builds. Tests pass. You deploy …then production crashes because a DLL won’t load. The code didn’t change, your tests didn’t fail, and nothing looks obviously wrong. The real culprit isn’t your application logic, it’s NuGet versioning. A seemingly harmless version number caused the wrong assembly to be restored, resolved, or loaded at runtime.
Versioning seems so simple (it’s just a number, right?), but with NuGet, it’s a bit of a minefield. There are five distinct, multi-part version numbers that can show up in a package. Each with its own formatting rules, and behaviors.
In this article, I’ll walk through how you can properly version your NuGet packages to speed up the development process, stay organized, and avoid those “wait, why did this break?” moments when deploying your NuGet packages and applications at scale in your organization.
How is Versioning used in NuGet Packages?
Like with all software, version numbers in NuGet are how developers tell one package apart from another. The difference between v5.3.2 and v5.4.0 isn’t just the number—it’s the bug fixes and enhancements made between those versions.
Without versioning, tracking what changed in which version gets messy fast. Eventually, the wrong version gets deployed to production, and nobody’s happy. With any software, versioning isn’t just about code: it’s a communication tool between developers, stakeholders, and other humans.
However, version numbers are also used by NuGet/.NET in a few different ways.
1. Managing Package Dependencies
NuGet packages rarely function as a standalone unit. Most of the time, they rely on at least one other package—called a dependency—to be installed first. Dependencies often have dependencies, which means installing a single package can result in a “dependency tree” of required packages.

This is where things start to get a little tricky: dependency resolution. Essentially, only one version of a NuGet package can be used at a time. So, if two different packages (let’s say A-5.4.2 and B-3.0.1) require different versions of another package (like C-1.2.1 and C-1.0.3), NuGet can’t resolve the dependencies. This means your project won’t build unless you pick different NuGet packages.
Thankfully, NuGet lets package authors use version ranges when specifying dependent packages. These can also get 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.
In our earlier example, Package A might say “anything from version 1.2.1 up to (but not including) 2.0.0 is fine,” while Package B might allow “any version that starts with 1.” This gives NuGet enough wiggle room to pick a version that keeps everyone happy, like C-1.2.1, which fits both requirements.
2. Loading Libraries Within Your Package
NuGet packages include .NET library files called assemblies. Your application loads these at runtime. When that happens, .NET looks for a specific Assembly Version packed into the assembly’s DLL file. If the assembly version doesn’t match what your application expects, you’ll get a nasty load error, and your app will crash.
Here’s where things get confusing: the assembly version number is totally separate from the package version number, and even follows a different format. This feels unintuitive, but there’s history there. .NET was created a decade before NuGet. Before then, assembly versions were the only way for applications to be tied to a specific version of a library.
Luckily, 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-1.2.1.0 to C-2.0.0.0 is okay -->
<bindingRedirect oldVersion="1.2.1.0-2.0.0.0" newVersion="2.0.0.0"/>
That said, binding redirects, like version ranges, can get complicated fast, causing additional problems if not done right. If something goes wrong, you probably won’t catch it until runtime.
3. Displaying Important Information
Additionally, DLL files can include 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 in the Details tab in Windows Explorer:

The biggest problem this can cause is 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.
| Version Type | Format | Usage/Display |
| Package | SemVer (e.g. 1.0.3) -or- 4-part Number (e.g. 2.3.25.1) | Used to identify one version of a package from another. |
| Dependency | Interval Notation (e.g. 1.2.1,2.0.0) -or- Floating Notation (e.g. 1.*) | Used during build time to resolve dependencies and find needed packages. |
| Assembly | 4-Part Number (e.g. 2.3.25.1) | 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
While NuGet supports four-part version numbers, Microsoft now recommends using Semantic Versioning (SemVer) instead—and for good reason:
- The “SemVer2 spec” is very detailed and clearly defines what each of the 3-parts means, 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 backward-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 backward compatible (2.1.4).
This structured approach makes it easier to manage versions and communicate changes effectively across teams.
💡 Best Practice: Fixed Assembly Versions
Before NuGet, assembly version numbers were important: libraries had to be installed in the server’s Global Assembly Cache before .NET applications could use them. But these days, things work differently. NuGet just copies the assembly DLL files next to your application/s executable files, so the entire application (libraries and all) gets deployed together. This makes the assembly versions a relic of the past.
For most modern .NET applications, using “fixed” numbers (like 9.9.9.9) for your library assemblies means that you’ll never run into assembly load errors at runtime.
// Fixed version number
[assembly:AssemblyVersion("9.9.9.9")]
By locking the assembly version, you also reduce the need for binding redirects in your configuration files. Which, if you think about it, is effectively doing the same thing: after all, binding redirects just tell .NET to ignore the actual number, anyways.
🔄 Alternative: Match the Assembly Version and Package Version
For some libraries, using a fixed number for your library isn’t practical, so instead, 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
[assembly:AssemblyVersion("5.4.2.0")]
💡 Best Practice: File & Informational Versions
When using a fixed assembly version number, it’s a good idea to still embed the number in the DLL file using the assembly version file attribute. This will make it easier to debug on the off chance you ever need to verify the right library was deployed with your application.
// Package Version number 5.4.2
[assembly:AssemblyFileVersion("5.4.2")]
[assembly:AssemblyVersion("9.9.9.9")]
You should avoid using the assembly informational version; it’s only displayed in a few places in Windows and only gets used if the assembly file version is not present.
💡 Best Practice: Don’t Deploy Pre-release Packages
A key part of the SemVer standard, pre-release labels let developers know that a NuGet package is unstable. It’s simple: if a package version includes a dash and descriptor after the patch number (e.g., 5.4.2-beta), it’s considered pre-release or unstable. Packages without these pre-release labels are considered stable.
To help prevent untested library code from sneaking into production, a good rule of thumb is to block pre-release labelled packages from being deployed in production applications. The good news? Setting this up in your CI/CD pipeline is straightforward.
💡 Best Practice: Build Pre-release Packages
When you build a NuGet package—whether manually with a tool like NuGet Package Explorer, or as part of a NuGet CI/CD process—you should always assign a pre-release label. This signals that the library within the package is unstable, and should not be used in production deployments.
Packages must have unique numbers, and prerelease labels with “dot identifiers” are an easy way to manage this. Since there’s no standard for what these identifiers should be, it make some trail and error to find what works for your team. A popular method is to use “ci” along with a sequence number or DateTime string.
For example:
- 4.2-ci.4 uses “4” as a sequence number to identify it’s the 4th build for 5.4.2.
- 4.2-ci.202506171055 uses a long Datetime string for when the package was created.
💡 Best Practice: Repackage After Testing
If your team always builds pre-release NuGet packages, but never ships them to production, you’ll need a repackaging process that turns pre-release packages into stable ones.
Repackaging creates a new package from an existing pre-release package, using the exact same contents, but changing the version number. Since NuGet packages are just .zip files, you can repackage them 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. You can script your CI/CD tools to include repackaging in the pipeline (our BuildMaster can do this), or you can use a package server with built-in repackaging functionality, like ProGet.
Beyond Versioning and NuGet
Learning proper NuGet package versioning means learning to handle five distinct version numbers, but putting in the time now will reduce a lot of headaches in the future, and may even save you from production problems like application crashes.
Implementing the best practices for each version number, such as SemVer and fixed assembly versions, is a good way to dodge potential issues like unstable packages reaching production and assembly errors, so start using these tips now, since you’ll likely be versioning many NuGet packages down the road.
Those were a lot of best practices we went over in this article! I recommend saving or bookmarking it for when it’s needed later. If you want even more insights, why not grab our eBook, “NuGet at Scale”. It includes everything we just covered, plus it’s packed with info on NuGet Metadata licenses, connector filters, and more! Download your free copy today!