Top 10 NuGet (Anti-) Patterns
NuGet has become the de facto standard for package management in Microsoft .NET (see NuGet.org). Even if you’re not currently using NuGet, you probably know of development teams and organizations that are using it or looking into it. In this article, I describe the top 10 anti-patterns I’ve discovered when helping customers onboard NuGet. I discuss each anti-pattern and provide an alternative approach, or rule, that should help you avoid the common pitfalls of getting up to speed with NuGet.
The introduction of NuGet into an organization often starts with one or two enthusiastic developers trying it on their own – often providing a convincing proof-of-concept. They expect their organization to experience a fast transition from “dependency hell” toward a structured and organized NuGet package repository. Some of them succeed; others face the inevitable barriers presented by most large enterprise environments. My goal in this article isn’t to talk you into using a package management tool such as NuGet. Rather, I want to make sure that if you are planning to use NuGet, you’re aware of some of the important technical caveats about it before you roll it out. Some of the anti-patterns are especially applicable to enterprise environments, and some are less applicable to open source projects. Regardless of your situation, knowing how to react or mitigate issues that can occur with NuGet will put you in a much better position to introduce it into your development environment.
As with any development tool or process, a package management solution impacts the overall Application Lifecycle Management (ALM): Everything always has to be put in context. One point I want to stress is that NuGet can’t fix a process that is broken. NuGet is merely a tool – an excellent tool – that can help you improve your ALM processes. Because the NuGet packages you consume are released components, you’re forced to come up with a proper versioning strategy, which in turn can affect your source control structure and branching strategy. When you start creating your own NuGet packages, you’ll soon end up questioning your component’s interdependencies, which can affect its architecture as well as the impact on your Continuous Integration (CI) or Continuous Delivery (CD) processes. Because package management affects all these areas, embracing NuGet in your development environment is an opportunity to improve your processes by introducing more structure and pragmatism into the way you work.
These 10 topics describe my recommendations to avoid the most commonly encountered anti-patterns:
- Use Semantic Versioning.
- Don’t use a build number.
- Use leading zeros in the numerical suffix to auto-increment prereleases.
- Integration packages must have their own repository.
- Never delete a package from a public feed.
- Don’t depend directly on NuGet.org.
- Don’t use a different package ID for CI/nightly build packages.
- Sound the alarm whenNuGet adds binding redirects.
- Don’t inject sample code or configurations while installing release packages; use a Sample package or provide a readme instead.
- Undo any side effects of package installation during uninstallation.
A NuGet package is identified by a package ID and a version. The package ID must be unique but can have multiple versions. The entire upgrade experience of NuGet packages relies on the version of the package. Many issues arise from the improper versioning of NuGet packages—just ask the IT department of an organization that implemented NuGet without thinking through the versioning requirements. It’s no wonder that three out of the 10 anti-patterns in this article relate to package versioning.
Rule #1: Use Semantic Versioning
The Semantic Versioning (SemVer) specification, authored by Tom Preston-Werner, is available at SemVer.org. Semantic versioning isn’t a new idea, but it is unique in its simplicity and implementation. Even if you don’t follow the SemVer specification exactly, you probably use a similar versioning system. SemVer is a pragmatic approach toward versioning that clearly indicates what impact the changes a package update will bring to the consuming application. This means that you’ll already have a good idea of how the package will affect your application, even before consuming it. This knowledge should help you estimate the effort required to deal with breaking changes when planning an upgrade.
The NuGet versioning algorithm is very close to the SemVer specification. By very close, I mean that NuGet follows SemVer in its entirety, except when it comes to build numbers. Because the SemVer specification is still a prerelease specification and the build number specification is still subject to change, the NuGet implementation doesn’t yet consider the build number in all cases.
The problem is that very close isn’t good enough. To apply proper semantic versioning and maintain a package upgrade path, you need to distinguish between release packages, prerelease packages and packages that are built during CI or nightly builds. In this article, I refer to nightly builds as integration packages, because they are created only to check whether the package is built correctly, preferably using a test client to check for integration errors.
When you’re working in an Agile methodology (such as Scrum or Kanban), you usually don’t have a fixed scope between two public releases. SemVer makes the version dependent on the changes you made, which are defined by the scope of your release. In other words, a fixed scope results in a fixed version. As a direct consequence, applying SemVer in an Agile methodology means that you don’t know the version of your next release until the date your release is ready—that is, that you know the version number only when a human makes the conscious decision to press the release button.
Table 1 illustrates how these types of NuGet packages could be versioned in order to maintain a smooth upgrade experience for its consumers. Optional parts are between [angle brackets].
|Package Type||Possible Versioning|
|NuGet pre-release package||major.minor.patch-prerelease|
|NuGet release package||major.minor.patch|
Table 1 Versioning Schemes
Using Semantic Versioning for release packages ensures that a consumer knows what to expect before upgrading. If your package contains breaking changes (indicated by a major version increment), the consumer knows to plan accordingly. If your package indicates it is fully backward compatible and contains new features (minor version increment) or internal bug fixes (patch version increment), the consumer expects that the package can be upgraded anytime. NuGet itself is also a consumer. Update-Package has a –Safe option that relies on proper version semantics.
At all times, two releases must differ in version by only a single increment of one version number.
Rule #2: Don’t use a build number
Referring to Table 1 and extending the rule of applying SemVer, you shouldn’t use a build number on prerelease or release packages. In fact, I recommend not using a separate build number at all.
For releases, there is no hard constraint not to use a fourth (build) number. It is technically possible to use major.minor.patch.build as a release version. In my experience, however, the build number is usually equal to 0 when following SemVer, because at the very least you’ve incremented the patch number (which requires you to reset the build number to zero). SemVer dictates that all version numbers to the right of the incremented version number be reset to zero. In short, the build number has no meaning for releases.
For prerelease packages, the reason for not using a fourth build number is simple: NuGet doesn’t support build numbers in prereleases. You can’t create a NuGet package in the format major.minor.patch-prerelease.build or major.minor.patch-prerelease+build. Any other attempt to auto-increment a prerelease package using a build number will most likely fail because the version precedence algorithm is sorting prerelease tags in alphabetical order. This effectively means, for example, that v1.0.0-alpha2> v1.0.0-alpha10> v1.0.0-alpha02 (which brings me to the next rule, as you’ll see in a bit).
But let’s say you want to use an auto-incremented build number for your CI packages. NuGet doesn’t currently support the SemVer notation for build numbers (prefixed with a “+” sign). NuGet does, however, support a version number with four parts separated with a dot (major.minor.patch.build). If you assume there’s no problem in using this notation during continuous integration, however, you’re sorely mistaken. The caveat is that this notation isn’t considered a prerelease version. It would be treated as a higher version number than the prerelease packages. It would even be considered a newer version over a release package that uses the major.minor.patch versioning scheme, because the missing build number is assumed to be zero.
NuGet also doesn’t allow producing non-prerelease packages that are dependent on prerelease packages. This means that you wouldn’t be able to create CI packages when consuming prerelease packages during development.
The only workaround I have found so far is to ensure that all CI packages are prereleases, which brings me to the next two patterns.
Rule #3: Use leading zeros in the numerical suffix to auto-increment prereleases
This rule is a direct consequence of rules #1 and #2. I wouldn’t recommend creating hundreds of prereleases for a package, but in some situations, using prereleases can come in handy. That said, using a lot of prereleases usually indicates that something else isn’t in place. For example, you might want to take a closer look at your release cycles. Why do you want to create a prerelease rather than actually releasing your package? If you don’t trust your release package, improve your testing. You should always aim for the shortest time to market. If you do need to create many prereleases, be aware of the limitations explained in the first two rules and make sure you don’t mess up package precedence.
Prerelease package versioning using an auto-incremented number is possible only if you use the number as a suffix to the prerelease tag and allow for enough leading zeros as placeholders for larger numbers. You also have to reduce the number of leading zeros, depending on the number of digits the auto-numbering requires. This ensures that you always have an equal number of characters and so you won’t break the order in which your packages are being sorted or consumed.
Here is an example, sorted in ascending order of precedence:
Now try to position the package with version v1.0.0-alpha15 in that list, and you’ll get my point: v1.0.0-alpha002 < v1.0.0-alpha15 < v1.0.0-alpha256.
Now you can see why this strategy is error-prone unless you always use the same number of numeric characters as a prerelease tag suffix. A better approach is to put your auto-incremented package builds onto a separate repository, inaccessible for general consumption.
You can solve a lot of common problems by having a proper strategy toward package repositories. For a professional environment, I strongly advise you to set up your own package repository, whether that is a simple network shared folder or a full-fledged Gallery implementation, and whether internally or externally hosted. As long as you have full control over what packages and which versions of those packages are available, and who has which permissions on what repository, you should be fine.
Rule #4: Integration packages must have their own repository
How you organize your package repositories is entirely up to you, but I want to make one recommendation: Ensure that you keep consumable packages (releases and prereleases) separate from the packages you produce during CI or nightly builds.
As a direct consequence of applying the versioning scheme in Table 1, all subsequent CI builds after a release will result in a newer package version than the actual release (version 1.2.1 < 1.2.1.x). You want to avoid exposing these development builds, which would appear as package updates to your consumers.
Because integration packages aren’t meant for public consumption anyway, you might as well drop them on a separate repository, available for internal testing or consumption by test clients.
Rule #5: Never delete a package from a public feed
A package repository is public as soon as a client can consume a package from it. Whether a repository is public has nothing to do with where the repository is hosted. It also has nothing to do with internal consumption or external consumption. As soon as someone acting as a client of the package is able to consume it, your repository is public. That’s why you should consider every public repository a production repository.
For this reason, you should never (as in not ever – a puppy will die if you ignore this rule!) delete a package from a public repository. If the consumer has enabled package restore and thus isn’t checking in the consumed packages to a version control system, deleting a package will cause a nightmare scenario for its consumers.
Rule #6: Don’t depend directly on NuGet.org
You shouldn’t depend directly on the NuGet.org Gallery feed if you want to have full control over what packages are consumed, available or approved for consumption in your organization, especially if you’re in a large corporate environment. Instead, mirror all the packages you need from the NuGet.org feed onto your own repository. For example, MyGet.org has out-of-the-box support for this mirroring. It also lets you push prerelease packages to a production feed, manage users, filter proxies of existing feeds and more.
It’s also a good idea to have a fallback repository in place just in case NuGet.org goes down (although Chuck Norris has promised not to roundhouse kick those servers again).
So far, the rules mentioned are related to each other and impact your development process. The ones that follow provide more general guidance on avoiding common snags.
Rule #7: Don’t use a different package ID for CI/nightly build packages
Sometimes, people try to avoid the multi-repository or separate versioning approach by changing the package ID. Having a SamplePackageCI or a SamplePackageNightly – assuming you want to release a package with an ID equal to SamplePackage – isn’t really a solid plan.
Why not? Simple: There’s no upgrade path. If you do expose your nightly builds (for example, on the MyGet Gallery), consumers will have to uninstall your package before they can install the release. Worse, if they don’t pay attention, the packages could be installed next to each other.
Rule #8: Sound the alarm when NuGet adds binding redirects
Pay attention whenever you notice that NuGet is adding binding redirects during package installation. It’s a sign that NuGet is trying to redirect the assembly binding for the installed binaries (or one of its dependencies). In this situation, there’s a potential risk for conflicts during assembly resolution. Most of the time an issue won’t come up, but occasionally you might find yourself wearing a pink sombrero in dependency hell.
In short, investigate why any binding redirect has been added, and check whether you can resolve it by aligning your dependencies to a common version.
Rule #9: Don’t inject sample code or configurations while installing release packages; use a Sample package or provide a readme instead
I’m annoyed when I install a package and then notice that some sample configuration sections made it into my application’s configuration file. Sample code can also appear. Such sample injections might be useful the first time someone installs your package, but users will be peeved if they have to remove them on every update of your package.
Instead of such injections, use a Sample package or provide a readme file that NuGet will open automatically when installing your package in Visual Studio. If you scan the NuGet.org Gallery, you can see that most package producers use the convention of suffixing the release package ID with .Sample.
Rule #10: Undo any side effects of package installation during uninstallation
Although this rule sounds obvious, many forget to follow it. When designing a NuGet package, most developers pay more attention to how the package gets installed into the consuming solution and less attention on ensuring that the package uninstalls properly.
You must be especially careful about this rule when you perform custom installation steps in the install.ps1 PowerShell script. Whenever you create an install.ps1 file, you should have the reflex of immediately creating an uninstall.ps1 script as well.
The NuGet extension deals with the installing and uninstalling of content files, so if you’re using NuGet you don’t have to worry about intervening manually to follow this rule. Just look for warnings in the Package Manager Console when uninstalling the package if you modified any of the injected files.
In this article, I covered 10 common pitfalls that can occur when you use NuGet and then attempted to provide you with a way out. These rules have worked for me and for my customers, and I hope they help you as well. Just remember: Define your strategy before introducing NuGet into your environment.
Xavier Decoster works as a Technical Consultant for RealDolmen (Belgium) and is one of the authors of the world's first NuGet book (“Pro NuGet,” from Apress). He is also a cofounder of MyGet.org, a NuGet-as-a-Service product built on top of Azure. You can contact Xavier on Twitter (@xavierdecoster) or through his blog at xavierdecoster.com.