Inaccurate or esoteric versioning of your NuGet packages isn’t just a bad practice; it’s a dangerous one. When package names aren’t crystal clear, prerelease packages can slip through into production and lead to broken systems, security vulnerabilities, poor user experience, and other risks.
Proper versioning is even more important as you begin the move to .NET 8, because agility, speed, and reliability are ingrained in .NET5+ (.NET 5 to .NET 8). You won’t be able to keep up with the pace of the new .NET if you’re not following Continuous Integration/Continuous Delivery (CI/CD) practices.
Using CI/CD for NuGet packages makes enforcing strict Semantic Versioning simple and speedy. In this article, we explain:
- why .NET5+ may seriously disrupt organizations that use poor versioning;
- what Semantic Versioning is and why you should use it;
- how you can use automation to enforce strict versioning easily.
Considerations for Migrating NuGet Libraries to .NET5+
.NET 5 marked what Microsoft calls “.NET Core vNEXT” and is the sole future of .NET. There are both practical and philosophical things that make .NET5+ such a big change.
Practically, .NET5+ will not be compatible with .NET Framework. You will therefore have to make decisions about every application in your organization—deciding if the application will migrate at all, and if it will, how you will migrate your libraries (because the new .NET probably can’t use your old libraries). Because the lifecycle of .NET Framework will not be end-of-life until almost 2030, certain applications (like ASP.NET Web Forms) will likely never need to be rewritten for .NET5+.
Philosophically, since .NET 5, Microsoft will move at an unprecedented release speed. As we discussed in detail in our long-term support blog, odd-numbered .NET5+ versions will be short-term support only, while even-numbered releases will be long-term support. And the time between releases will be significantly shorter than past .NET platforms: Microsoft plans to release a new major version of .NET5+ every November starting in 2020.
This is a fundamentally agile way to develop and release software. If your organization is not prepared to move this fast, you may be quickly overwhelmed. You can be as agile, fast, and reliable as your new .NET platform by embracing Semantic Versioning.
Versioning versus Semantic Versioning
Versioning simply means a process to name software and indicate differences. One organization might have a rule that the version numbers go by monthyear.day (“Release 1120.5”) while another might change the “major” version each year (“Release 2.x” after “Release 1.x” the previous year). The concept of versioning does not inherently contain rules.
There are so many problems that come from random or unstrict versioning, including:
- releasing unready versions to production
- duplicated work, because someone accidentally uses an old version
- problems in production (“But it worked on my workstation!”)
- “dependency hell” (using a package means using its dependencies, which you may not realize are incompatible with other versions)
All these problems slow organizations down in a business world that demands speed and reliability. Semantic Versioning is a system meant to minimize (or even eliminate) these problems because it is based on the fundamental principle of package immutability.
What is Package Immutability?
“Package immutability” is just a fancy way of saying “do not edit!” What distinguishes a “package” from a random zip file is that, once a package has been created, it should never be modified. Since the version number is part of the embedded metadata of a package, editing the same version will create confusion or even conflicts. While it might be okay to outline, draft, edit, and re-edit the blog article in a Microsoft Word file named “Blog for Inedo.docx,” packages must never do this.
Instead, use a new version number to indicate when a package has been altered. Continuing the Microsoft Word analogy, it’s the difference between “Save As” and “Save”—while “Save” overwrites the previous version in the same file, “Save As” preserves the old version, clones it into a new file that you then edit and rename. If Package 1.0.4, for example, may need tweaking; you would make your edits in the re-versioned 1.0.5. Following this same best practice, public feeds like NuGet.org don’t permit packages to be overwritten; any changes to a package come through as a new Semantically Versioned version of that package.
What is SemVer?
Semantic Versioning (or SemVer) describes a strict software naming system to indicate backward compatibility or incompatibility.
SemVer requires three digits: Major.Minor.Patch. Major releases (2.0.0) indicate changes that will be incompatible with previous versions. Minor release (2.1.0) adds 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).
Additionally, SemVer includes “prerelease” indicators: a dash and descriptor after the patch spot (2.1.4-ci). These are used to describe packages that haven’t been tested yet and thus are not “stable.” Common prerelease versions are “Continuous Integration” (-ci), “Beta” (-beta), and “Release Candidate” (-rc). Per SemVer rules, prerelease versions should never be deployed for production use.
Why is SemVer a Best Practice?
There are three major qualities that make SemVer a best practice, and all come down to human communication:
- It doesn’t require extensive training for new staff to understand.
- Its strict rules leave no room for interpretation by different staff members.
- It is practical and obvious enough for business stakeholders to understand with minimal explanation.
Because the SemVer site exists (and because its rules are so straightforward), new staff can be trained very fast. In fact, new hires are likely to already know Semantic Versioning if they’ve worked before in software development. Moreover, setting up your CI/CD pipeline to create stable packages from prerelease is straightforward.
By contrast, if your organization or team uses a specialized naming system that Juan wrote and then Juan leaves the team, you are at the mercy of Juan’s documentation of the system to learn it and teach it to others.
Almost Eliminates Confusion
SemVer’s naming system is very, very clear, and the website explains rules and guidelines in detail. If everyone follows the SemVer rules, it’s nearly impossible to confuse different versions or to improperly name versions.
By contrast, if you use a specialized naming system, the rules may be much less obvious, and an uninitiated person may make costly mistakes.
Easy for Non-developers to Understand
Because the rules of SemVer are very strict and very simple, they are easy to explain to a business stakeholder. Clear “translation” between IT concepts and non-developers can minimize confusion and help stakeholders understand IT’s needs, bandwidth, and so on.
And for .NET5+ migration, there is a fourth reason that SemVer is a best practice: NuGet packages must be versioned with the SemVer scheme.
NuGet Dependencies and SemVer
What has made NuGet so popular is the ability to build off of someone else’s code. This eliminates the need to write the same thing over and over again or to be an expert in everything. But sharing also means layering. Microsoft created this diagram to explain:
Sharing packages creates a “dependency tree”— you build off someone’s code that was built off someone’s code, which was also built off someone else’s code, and so on. This is just as true for first-party packages as for third-party ones. Your internally built NuGet packages will develop complicated dependency trees over time—especially with .NET5+. Using just one package usually means taking many, many more packages into your package as dependencies. When this is unmanaged, you can get stuck in “dependency hell,” trying to dig through all your NuGet package dependencies to figure out where a break is happening.
Installing a NuGet package also means installing its dependencies. And as Microsoft explains:
“When multiple packages have the same dependency, then the same package ID can appear in the graph multiple times, potentially with different version constraints. However, only one version of a given package can be used in a project, so NuGet must choose which version is used.”
NuGet uses four rules to resolve dependencies: lowest applicable version, floating versions, nearest-wins, and cousin dependencies, which Microsoft explains in detail in their documentation with examples. For a “lowest applicable” version, for example, if you depend on 1.0.* (any patch version of 1.0), and the feed has 1.0.0-beta, 1.0.0, and 2.0.0, the application will use 1.0.0.
The rule names make this seem really complicated. The takeaway is that a set of rules governs how your NuGet packages will deal with dependencies. Knowing these rules can help you avoid “dependency hell.”
Simply put, NuGet was built to follow the rules of the three-place SemVer versioning rules.
Resolve SemVer and Continuous Delivery Tensions with Repackaging
SemVer is so practical, it may be hard to understand why developers may choose not to use it. One common concern is that Continuous Delivery is automated, but if packages are immutable, enforcing SemVer has to become a manual process. While it may seem difficult to reconcile package immutability, SemVer, and Continuous Delivery, using repackaging automates these best practices.
What is Repackaging?
Repackaging creates a new package from an existing package, using exactly the same content but changing the name. For example, Build 6 yields 1.0.4-ci for testing; once approved and repackaged, a stable version, 1.0.4, will be created for deployment in production.
Packages are just zip files. Repackaging a package means:
- downloading the package
- opening the zip file
- editing the nuspec manifest file with the new version
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 1.0.4-ci, you test it, and upon approval, you repackage it as stable version 1.0.4.
This is usually where people perceive tensions between repackaging (using best practices) and automation (Continuous Delivery), but the good news is that repackaging can be automated. You can write a script to tell your CI/CD tool to include repackaging in the pipeline (our BuildMaster can do this), or you can use a package server with built-in repackaging, like ProGet.
.NET5+ ushers in a new era of .NET development speed. To be able to keep up, start by going back to the basics. Using correct versioning—and automating this process by repackaging—will make your life easier as you move your NuGet libraries to .NET 8.
Just like following Semantic Versioning, making plans for .NET 8 at your organization doesn’t have to be overwhelming or complicated. We’ve built a guide on how to successfully migrate to .NET 8 and beyond. Get your free copy today!