When you’re publishing multiple Nuget packages from a single repository, it’s generally far simpler to just allow them all to re-version together. So a change to My.Package which moves it from version 1.2.2 to version 1.2.3, also moves My.Package.SomethingElse and My.Package.SomeOther to 1.2.3 as well, regardless of whether they were actually changed. Incrementing the patch version indicates a safe, backwards compatible change.
But the default behaviour of a call to ‘dotnet pack‘ will mean that as your project references are transformed into nuspec dependencies, 1.2.3 becomes the new minimum compatible version (ref). So if My.Package references the other two projects, you can’t use version 1.2.3 of My.Package with version 1.2.2 of My.Package.SomeOther. Which often isn’t a big problem, but can lead to some unnecessary work.
If you are publishing packages privately within a small enterprise, for example, you may find that you end up referencing some of your packages from multiple other packages. There will likely be some packages which find their way everywhere. This doesn’t sound like such a problem, until you need to update something which is reference by almost everything you publish.
Dependency inversion
Dependency inversion is a great way to avoid unnecessary dependencies between your Nuget packages. If you’re writing a tool which should make use of MongoDb, for example, and you already have a customised MongoDb package which you’d like to make use of. Instead of creating a dependency between the two packages, your new tool can define a package with interfaces, POCO’s, and setup extensions which represent storage behaviours. Then your consumer can be responsible for referencing your MongoDb package and making it work with those contracts.
The idea behind this shuffling around of dependencies is that your tool only depends directly on itself, delegating the job of referencing MongoDb to the consumer (or another package).
With this setup, the consumer is responsible for making sure the package versions it currently references work with each other. The consumer contains code to link everything together. And so versioning any package does not immediately force you to update everything everywhere.
Independent versioning
Another method of reducing the impact of changes, is to version each related package independently. Instead of My.Package and My.Package.SomethingElse both always incrementing from 1.2.2 to 1.2.3, each would only increment if its source code had been modified. This works if you organise your assemblies so ‘less volatile‘ assemblies never reference ‘more volatile‘ assemblies. You might extract My.Package.Abstractions, for example – which only contains POCO’s and interfaces. Such ‘abstractions’ packages are not particularly volatile, so other packages can depend on them without risking forced updates. If you have some types which are required almost everywhere, then these can be placed in an ‘almost never changing’ assembly which is repackaged very seldomly.
3rd party wrappers
If you find you need to use a 3rd party package in more than one of your internal packages, then you should probably introduce a 3rd party wrapper. This is a package you write to act as the one point of abstraction around the 3rd party for all of your packages and services. This allows you to publish your own POCO’s and interfaces, so consumers can depend on your own abstraction – reducing the impact of changes in the 3rd party.
When using this approach, it’s important to be careful about what functionality you expose. This is a great point of reduction, to make your abstraction provide just what functionality you need – further protecting you from possible downstream changes.
Most selective dependencies
One more thing to consider, to reduce the amount of churn caused by package updates, is whether your projects are referencing what they need in the most efficient way. If your assembly only needs to reference a few classes in My.Tool.Something, but is referencing the parent package My.Tool, then you’re unnecessarily exposing it to updates of My.Tool.
Summation
Package versioning is a pretty dry topic, and most of the time we’re focusing more on the capabilities our package delivers, and less on how it might cause dependency issues. Altering our code so it can be independently modified while still being consumed by multiple related processes can take longer, and most certainly brings its own level of complexity. But the danger of not paying attention to how updates will impact your consumers is an exponentially increasing amount of work required to implement each change. Just like normalising a database, correctly structuring your dependencies should be a no brainer.