This is part 2 of a short series of blog posts on Paket. In the first part, I presented the overall value proposition behind Paket and some (but not all!) of the features it provides. In this post, we’ll look at performing a migration of a simple NuGet-ified project over to Paket.
In a perfect world, you’d be using Paket with a clean solution. But more often than not, if you’re new to Paket, you’ll have an existing solution that’s NuGet-aware, and will want to try Paket out by converting the solution from NuGet to Paket. Luckily, you’re not the first person to have wanted to do this, so Paket has an easy migration process. There’s already an excellent step-by-step guide here, but I thought I’d show you the migration process with a real-life project and what to be aware of, since Paket is in many ways “stricter” than the NuGet client when it comes to things like version conflicts.
Working with an existing solution
To demonstrate this, I pulled down an existing single-project solution off of GitHub – the Azure Media Services Explorer. Ironically the solution doesn’t actually build for me on VS2015 straight from the repo, but never mind – it doesn’t actually affect this article, since we’re only focused on the NuGet dependencies themselves – there’s no need to open the solution in VS at all.
The solution comprises a single C# project with an associated packages.config file, which looks something like this:
Obviously, our objective is to remove this, and replace it with the equivalent Paket files.
Performing a Migration
A migration normally contains the following steps:
- Convert the solution over to Paket
- Fix any invalid package dependencies and commit in source control
- Simplify the dependency graph
- Optionally, update all packages to their latest versions
- Commit back into source control
In this example, we won’t have any invalid package dependencies to deal with – we’ll leave that for the next post.
1. Converting from Nuget
The first, and most important step, is the conversion itself. Paket’s conversion command is designed to give you a like-for-like replacement compared to your current packages.config files – in other words, the same versions of dependencies, but in Paket’s file format. The conversion process is very easy to do: we open up the folder of the repository in VS Code, open the command palette and call the convert-from-nuget command:
VS Code will automatically download the latest version of Paket and place it into a .paket folder for you, before calling the command. This command will read all package.config files in the folder, create a unified set of dependencies, and then replace the config files with Paket’s dependency files. Finally, it’ll download all the packages, and update references in any affected project files.
After a short delay, you’ll see output in VS Code with information about the migration process before it completes (you can also try this out from the command line).
At this point, there’s nothing more for us to do in terms of basic migration – we’ve successfully migrated over to paket, and can skip straight to committing into source control if we want. You’re done. But, before we do that, let’s look a little deeper – open the newly-created paket.dependencies file and you’ll see the following:
As you can see, all of the dependencies from the original packages.config file have been ported across. Unlike what we saw in the previous post, these dependencies have explicit versions and framework constraints on them, rather than “floating” dependencies i.e dependencies whose version “floats” to the highest version whenever you call update:
Remember – this is a constraint. Paket has many types of constraints that you can place on dependencies e.g. at least v3, between v3 and v3.5 etc. etc.. The constraint specified here says that KeyVault.Core is pinned to exactly version 1.0.0, and for at least net452. These constraints were ported across from the original nuget packages.config files by Paket in the interests of repeatability.
Checking for outdated packages
At some point, you’ll want to update to newer versions of your dependencies. Paket just so happens to have a command to check all your outdated dependencies, called outdated. If we run it, you’ll see the following:
This seems wrong. For example, WindowsAzure.Storage is at 7.2.1 in the dependencies file, yet according to NuGet.org version 8.2.1 is available. Why has Paket not seen this? Because all of the constraints we just mentioned. In other words, WindowsAzure.Storage is pinnned to exactly 7.2.1, and therefore Paket will never suggest a new version. So, let’s eliminate this restriction by removing the pinned version numbers from the dependencies file. Remember that Paket doesn’t need a custom GUI. Instead, just update the paket.dependencies file in whatever text editor you prefer!
Note: In this screenshot I’ve also replaced the per-package framework restrictions with a placed a single global TFM constraint at the “top” of the dependencies file.
Now we’ve done this, we can re-run outdated, which indeed shows a number of outdated packages which we could update to new versions that are proved to be compatible with one another according to all package constraints:
In other words, these updates might not be the latest versions of all of of those packages. Instead, they’ll be the latest versions for all packages that are compatible with each other. Before we do the update, let’s hold off for a while and look at another Paket command.
4. Simplifying Dependencies
Before we actually perform the update, we’ll take advantage of one of Paket’s features to make our life a bit easier and call the simplify command, which reduces the entries in dependencies file to just the top-level dependencies:
This is now much easier for us to reason about – all the transient dependencies have been automatically removed, leaving just the top-level ones are there for us to look at (compare this to the original packages.config). Of course, details of all the transitives are still stored in the paket.lock file (see below), which is unchanged – you just don’t need to worry about them any longer. Paket will continue to track them, and because the lock file is always committed into source control, you still get guaranteed repeatable builds.
4. Updating versions
OK. Now that we’ve simplified our dependencies, let’s get on with the update!
Recall that unlike NuGet, Paket will by default automatically update packages to the latest version available, unless pinned or restricted otherwise by other packages. Observe the results of running the outdated command from earlier, and you’ll notice that some of these changes are upgrades by major version numbers. For example, System.IdentityModel.Tokens.Jwt is currently at 4.0.3, but 5.1.4 is available on the NuGet servers. This could indicate a breaking change, and we might not want to do that just yet. And while a fully-specified nuget package might place a maximum version on a dependency, in reality few projects bother to do that. So, to prevent our upgrading to version 5.x of the Jwt package, let’s put an entry back into the paket.dependencies file to prevent it:
This tells Paket to allow updating System.Identity.Tokens.Jwt as long as the version remains within 4.x. If that package author respects SemVer, we won’t run the risk of getting any breaking changes. Sure enough, if we run outdated again, you’ll see that the package would only be updated to the highest available 4.x version:
A few words on Update
Once we’re happy with the look of outdated, we simply call the update command, which will promote all packages up to the latest version possible updating the lock file and pulling down the appropriate packages from NuGet. We wouldn’t be able to use install here, since we’ve not added or removed any dependencies.
It’s worth emphasising that with Paket, the default update command (i.e. with no parameters) updates all your dependencies to the latest version possible, whilst ensuring that the dependencies file obeys the constraints of all packages.
In other words – when performing an update, Paket never simply updates a single package in isolation. It’ll first check to ensure that whatever updates occur work within the constraints of other packages. You can opt to update a single package, but even this will still ensure that the version operates within the confines of the constraints of all packages.
Normally, you probably won’t need to bother with dependency restrictions like I’ve shown here. Instead, I would recommend the following for a migration strategy:
- Do a basic convert-from-nuget, which will normally give you an *exact* replica of your all original dependencies.
- Make sure everything still builds, and commit into source control.
- Once you’ve got a stable commit (with a clean repository), run simplify.
- Remove all remaining pins in the dependencies file.
- Commit again.
- update to the latest available versions of your dependencies.
- Check if your solution still builds and unit / integration tests still run.
- If it’s all good, you can commit the update; if not, find out what the offending package is. Rollback the dependencies file, and place any explicit version constraints as needed. You can then decide whether to update and fix your code to be compatible with the latest version of the dependency, or not.
- Periodically, repeat steps 6-8 (or alternatively, set up a CI build on a nightly basis that does this to checks if new dependencies break your build!).
This process allows you update all your dependencies quickly and easily in a very controlled manner.
5. Committing into source control
Once we’re happy with the latest versions possible, we can commit back into source control:
The main changes are:
- We’ve replaced the packages config with paket.dependencies, paket.lock and paket.references files.
- We’ve added the bootstrapper version of paket.exe (~50K) and msbuild paket.targets file
- We updated the csproj with new assembly references
A word on binding redirects
Binding Redirects are a complex and sometimes painful area of .NET. Luckily, Paket has very good support for them. Simply adding the line redirects: on to your dependencies file instructs Paket to figure out any required binding redirects, and then add them to any app.config files that are in the repository.
In this post, we looked at the basics of migrating a NuGet-enabled solution to a Paket-enabled one, including simplification and package constraints, both of which are usually needed. In the next post, we’ll look at migrating a slightly larger multi-project solution, and some of the issues you’ll be faced with. Until then!