]> Using and debugging Nuget packages locally 🌐:aligrant.com

Using and debugging Nuget packages locally

Alastair Grant | Tue 26 Jun 2018

Most Visual Studio users will have come across NuGet.  NuGet is a packaging system for code, you can pull in pre-built libraries from other authors.  It is fully versioned, so you can set your project to use a particular version, or just keep up to date with the latest published.  It also supports publishing of debug symbols, which is handy when wanting to step through code.

NuGet is natively supported by Visual Studio and Team Foundation Server.  Which makes building projects with it a breeze.  TFS will automatically pull down NuGet packages when building your code.  It makes sense to use this approach for internal libraries too.  Anybody can download and run a NuGet server locally, and TFS has one baked in.  Just add the URL as an additional stream into your Visual Studio settings to make use of it.

When building applications with internal libraries, you can then reference a stable build that's been tested instead of recompiling all your libraries each time.  It also allows you to split out your code repositories into logical silos.  What's not to like about it?

Making changes to your NuGet libraries

The irritation comes when wanting to change your libraries - nobody is a perfect programmer, and building new functionality, or fixing bugs, without seeing how that impacts consuming applications is nigh-on-impossible.  With NuGet, you have to make your changes, commit them to the server, wait for a build, and then update your applications with the latest version before you can debug.  Rinse and repeat for every evolution of your code changes.

This is clearly unsustainable and grinds your productivity to zero.

How to get around this?  There are a few suggestions, such as manually editing your csproj to have custom build targets.  And using an addon to Visual Studio to switch your references to local files.

I'd like to offer a third method, which is working quite well:

Global Assembly Cache

One of .NET's selling points was avoiding DLL Hell: where you had DLLs deployed into System32 and another application would come along and dump a different version over the top.  The solution to this was the GAC (Global Assembly Cache).  The GAC allows for system wide deployment of DLLs, with side by side versioning: MyLib.dll (v1) and MyLib.dll (v2) can both exist at the same time.  When a .NET application runs, it will use the version it has been built against.

The GAC has fallen out of favour these days: I suspect due to it being a bit fiddly to use with UAC enabled, and the idea of packaging a few MB of libraries with your application no longer frightens off users on dial-up (well, I'm sure it still does, but who uses dial up these days?).  Bandwidth and disk space are excessive these days not to worry about sharing DLLs.

But, the GAC is still very much there, and still very much the man in charge.  .NET locates referenced DLLs in a very specific way, and right after "The DLL is already being used", is the GAC.  .NET will check the GAC first and then move on to looking for files locally with the application.

How does this brief history of the GAC help us working with NuGet packages locally?  Well if we deploy our updated code into the GAC (without the need to commit), our consuming application will pick up this version of the DLL (assuming the assembly versions are the same) and ignore the copies of the DLLs that have been pulled down via NuGet.  Debugging works exactly the same way with a GAC'd DLL, providing the symbols are built, then it'll work.

When you're done with your local testing and are happy with the changes to the files, then you simply remove the DLLs from the GAC and your consuming application will revert to using the NuGet referenced files (which hopefully you remembered to update with your changes).

How to implement

As noted before, using the GAC is a bit fiddly, which might be why people don't use it so much.  In order to add a DLL to a GAC it first needs to be "strong-named", which involves providing a digital signature to your DLL (this is to avoid somebody else publishing MyLib.dll (v1) and overwriting yours with malicious code).  Fortunately, this step is easy.

In your Visual Studio, open your project properties, select the Signing tab, and select a key (existing or new) to sign your code with.  It's good practice to sign all your code, but if you do, make sure you keep the key secure.  Don't check it into a public source code repository, as it kinda defeats the point if anybody can impersonate your signature.

Next you need to get your DLLs into the GAC.  There are a few ways to do it, but the most reliable is to use gacutil, which is included with Visual Studio, and also freely available.  You're not going to want to faff about with running the command each time you do a build, so I simply put in a post-build task:

gacutil /i $(TargetDir)$(TargetFileName) /f

Every time the project is built, it will be added into the GAC.

And that's all there is to it.  You can now make changes to reference from NuGet locally without having to go through the pain of republishing NuGet packages.

Gotchas

There is a catch to this approach, and that's when you update signatures in you referenced DLL and what to see those changes in your consuming project.  At runtime, .NET will select the file from the GAC, but in Visual Studio, the hint path is used to reflect the DLL for information.

In order to work around this, simply delete the DLLs from the ./packages folder locally.  Visual Studio will then hunt around elsewhere to find the DLLs, and find them usefully in the GAC.  You can leave these clear until you want to go back to using NuGet, and then you just need to restore the package - or more likely, upgrade the package to your newly deployed version that you've finished debugging with.

The other gotcha is assembly versions - due to strong naming, .NET will only reference a single version of a DLL.  It is fairly common to have your build server automatically version builds, if you're not checking those versions into source control, then it is likely that your local project has a different version to the NuGet version.  You might still be able to work around this by deleting the local packages as above and rebuilding your consuming project to link to the only version it can find - the one you're working on.

Breaking from the voyeuristic norms of the Internet, any comments can be made in private by contacting me.

Related