NuGet is a decent tool if you use it in exactly the way Microsoft envisages… but unfortunately, like many products/frameworks in their ecosystem, it suffers from Microsoft Tunnel Vision™. At work, I’m attempting to properly solve a problem which has been plaguing us for a while now, once and for all. You might find it referred to elsewhere as the nested solution/branch problem, the HintPath problem, the per-solution problem, the repositoryPath problem etc… it seems people have been complaining about it, and to little avail.
The fundamental issue is that NuGet packages are stored relative to the solution (usually in $(SolutionDir)\packages), and references to DLLs in those packages are project relative. So, if you have a project which is added to multiple solutions which live in different folders, the project’s assembly references will be valid in one solution, but invalid in the other. To illustrate, here’s our typical code structure:
/MyFooBar MyFooBar.sln /ReusableLibrary ReusableLibrary.sln /UsefulWidgets UsefulWidgets.csproj /packages /NLog NLog.dll /Foo Foo.csproj /packages /NLog NLog.dll
Our common reusable library, ReusableLibrary.sln exists in its own branch. Since we want to use it in MyFooBar.sln, we branch it into a folder at the source level. Our library projects were already members of ReusableLibrary.sln and are now also members of MyFooBar.sln. NuGet ‘packages’ folders ideally should not be checked in. This is an extremely common and sensible branching structure.
But look at what happens when UsefulWidgets is made to reference NLog.dll, acquired via NuGet. If added in the context of ReusableLibrary.sln, UsefulWidgets will have a reference to ..\packages\NLog\NLog.dll. But when the library is opened in the context of MyFooBar.sln, that packages folder will not exist, and the reference will be invalid. NuGet will pull in the same NLog package at /MyFooBar/packages/NLog, for which the correct reference would be ..\..\packages\NLog\NLog.dll. This seems like a pretty big design flaw and oversight to me. I’m not too sure how other people deal with this, but these are the candidate solutions we’ve evaluated.
The checked-in packages solution
This is the solution we used for over a year until recently. It involves checking in all NuGet packages and disabling package restore. This ensures that, upon getting latest, the packages always exist where projects expect them to exist. Problems with this approach include:
- Developers need to remember to check in packages. The moment they forget, the build breaks.
- Packages must only be added to projects in the context of the innermost solution they belong to. That is to say, if I want to add a NuGet package to UsefulWidgets.csproj, I need to do it in the context of MyReusableLibrary.sln. Otherwise, the new package would be added to the outer packages folder which does not exist when ReusableLibrary is opened in isolation in its own branch (or anywhere else it has been branched). Annoying.
- Someone might accidentally enable package restore.
- Checking in NuGet packages is generally frowned upon. I’m sure this has been discussed at length elsewhere, so I won’t get into it.
The HintPath hack solution (ding ding ding!)
This is the solution we’ve just switched to. It involves using NuGet as normal without changing anything, and without checking in packages. But whenever an assembly reference is added to a project as part of a NuGet package being added, you must manually edit the project file and change the HintPath (the path to the DLL) from being project relative to solution relative. In the above case, the reference would change from ..\packages\NLog\NLog.dll to $(SolutionDir)packages\NLog\NLog.dll. Provided package restore is working as it should, this reference should always be valid. From our short trial period, this seems like the least bad solution. Problems include:
- The obvious hassle of having to hack all NuGet originated references. Fortunately, this could be automated via a Powershell script (or perhaps as part of the build process). And unless you’re adding references galore, it’s not terribly onerous.
- In newer versions of NuGet, package restore only happens in the context of Visual Studio. So in order to get your build server to fetch packages on the fly as is required, you’ll need to edit your project files and add a task to invoke NuGet and make it do exactly what VS is doing (a call to nuget.exe restore).
- In some cases (e.g. using MSBuild to build a project in isolation), the $(SolutionDir) environment variable might not exist.
The single shared repository solution
NuGet allows you to specify repository paths via solution level nuget.config files. So you could set things up so that all solutions put their NuGet packages in the inner repository (i.e. ReusableLibrary\packages). So, you’d have a single stable repository which exists in all contexts. This solution is not viable for us because:
- The repository becomes polluted with packages which are only relevant to outer solutions. Essentially, the inner library repository ends up holding the set of all packages used by the inner library and everything which uses the library.
- This approach fails as soon as you have another nested solution whose projects have the ‘must work in multiple contexts’ requirement and does not lie on the repository’s root path (i.e. it does not contain the repository). Consider what happens if you branch in AnotherReusableLibrary at the same level as ReusableLibrary.
The ‘Don’t Use NuGet’ solution
Owing to our frustration with this issue, we were about ready to abandon NuGet altogether! After all, the old school method of manually checking in DLLs to a stable location might be slightly slow, archaic and cumbersome, but at least it’s reliable – it simply works without any hidden surprises. And it’s not like we have a huge number of dependencies anyway.
But, I like NuGet too much. It’s a step in the right direction for the M$ stack and it pleases me to be able to have the nice things that Ruby/Java/Linux/whatever developers have had for a long long time prior. Moreover, MVC is virtually married to NuGet nowadays. Divorcing them every time you create a web project would be a PITA.