How to Debug NuGet Packages with Symbols and Source Link Painlessly


Eric Seng

Eric Seng


What are NuGet Package Vulnerabilities and How to Manage Them 09th October, 2023

Why You Should Create a Package Approval Workflow for  27th September, 2023


How to Debug NuGet Packages with Symbols and Source Link Painlessly

Posted on .

Debugging your NuGet packages can be a total pain. In fact, it’s one of the main reasons that development teams are hesitant to break apart their monolithic .NET solution.

Unlike a referenced library project, you can’t easily step-into a NuGet package’s code to debug it. On top of that, when you need to make a simple change to an interface or method, you need to rebuild the whole package. Monoliths and copy/pasting code just seem way easier.

Fortunately, there’s a pretty easy solution to this problem; NuGet Symbol Packages. In this article I’ll discuss:

  • Demystifying Symbols and Source Serving (Source Link)
  • Creating NuGet Symbol Packages with Source Link
  • Legacy Symbol Formats & Packages
  • Configuring Visual Studio to Debug Symbol Packages

I’ll also share a simple alternative to using Symbol Packages that could make your life much easier.

Demystifying Symbols & Source Serving (Source Link)

The secret behind debugging into NuGet packages is Symbols and Source serving. There are four different technology “layers” that come together to make this happen.

But it’s a bit complicated, and it took me a little while to wrap my head around how it all works. Especially after reading all of the outdated — and often incorrect — articles.

I’ll do my best to explain the pieces and how they fit together as clearly as possible.

What the heck are Symbols, anyway?

You’ve probably seen .pdb files in your bin directory after building your project, alongside your .dll or .exe files. These .pdb files are “symbol files”, and they essentially map compiled assemblies (i.e., the .dll and .exe files) to the source code (i.e., the .cs files) that was used to build them.

Visual Studio uses these .pdb files to let you step-through code as it runs in Debug mode on your machine.

Even outside of interactive debugging, .pdb helps to provide more information on a stack trace when an application crashes. This is why you’ll often see the full disk path to the original code file when your application errors on a different machine.

What is a Symbol Server?

Symbols files actually predate NuGet (and .NET as a whole) by at least a few decades. They were originally designed for when disk space was at a premium, and symbols were rarely included in released software. This made debugging a pain.

This is where a Symbol Server came in. When building a library, the compiler will embed the same random, unique identifier (basically a GUID) in both the .dll and .pdb files. If you uploaded the .pdb file to a Symbol Server, you could later retrieve it using that unique identifier embedded in the .dll file.

That’s how a debugger like Visual Studio can load symbol files when an application crashes; you don’t need to have all of the .pdb files for every .pdb on disk. Visual Studio just requests it on demand from the Symbol Server.

What are NuGet Symbol packages?

A symbol package is basically just a NuGet package that contains symbols (.pdb files) instead of libraries (.dll files) and some different metadata. They also have a different file extension: .snupkg instead of .nupkg.

The nuget.exe or dotnet CLI will create a symbol package at the same time as a normal NuGet package when you specify the SymbolPackageFormat argument. For example:

nuget pack MyPackage.csproj -Symbols -SymbolPackageFormat snupkg
dotnet pack MyPackage.csproj -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg

After running one of those commands, you’d end up with MyPackage.nupkg and MyPackage.snupkg in your working directory: one would have the .dll files, and the other .pdb files.

The regular .nupkg package is then pushed to a NuGet feed, and the symbols .snupkg package is pushed to a symbol server.

Remember how a .pdb will map compiled assemblies (i.e., the .dll and .exe files) to source code (i.e., the .cs files)? This “map” requires having the exact version of the code files that were used to build the assemblies. So if you’re using a symbol server… where do the code files come in?

This is where Source Link comes in. When you build a NuGet package with Source Link enabled a Git Repository URL and Commit ID will be embedded in the package metadata. This allows Visual Studio to locate the required code files for debug time.

To enable Source Link in your own .NET project, you just need to set a few properties and then add a NuGet package specific to your Git repository (e.g. Microsoft.SourceLink.GitHub).

Legacy Symbol Format & Packages

Believe it or not, NuGet symbol & source serving was even more complicated just a few years back. You may run across some of these older formats and packages, so I’ll explain them real quick.

Legacy Symbol Packages (.symbols.nupkg)

While .snupkg symbol packages are the norm these days, there are also legacy symbol packages that have a different file extension (.symbols.nupkg). In addition to .pdb files, legacy symbol files will often include source code and .dll files.

Microsoft PDB (Legacy Symbol Files)

There are actually two different types of .pdb files: Microsoft PDB (aka Windows PDB) and Portable PDB. You can’t really tell the difference by looking at the file name, but they’re completely different file types.

Microsoft PDB is an ancient technology, dating back to the early 1990’s. It’s used for everything from native driver binaries to .NET Framework assemblies. But it only works on Windows and was never intended for cross-platform (Linux) debugging.

Portable PDB is much newer, and was a complete rewrite by the .NET team. As the name implies, they are platform portable and work on Linux and Windows. However, they only work with .NET libraries – which means if you’re building native (C++) libraries for Windows, you’ll still need to use the Microsoft PDB format.

These days, you probably won’t come across too many Microsoft PDB symbol files, especially if you’re working with .NET libraries in NuGet packages.

Legacy Source Server

Before Source Link became the standard for NuGet packages, a much older technology called Microsoft Source Server was required to retrieve the exact version of the source files that were used to build the library. It’s mostly a dead technology these days, as it was built for the era when source control was not Git or even web-based. But if you’re using TFVC or SVN, it’s pretty much the only option.

How to Debug NuGet Packages with Visual Studio

As I mentioned earlier, creating NuGet Symbol Packages is relatively easy. Just specify the SymbolPackageFormat argument when running the nuget.exe or dotnet CLI.

dotnet pack MyPackage.csproj -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg

From there, you just need to publish your .nupkg package to a NuGet feed and your .snupkg to a Symbol server. This will obviously require having a capable private NuGet Server.

To demonstrate how this works, I’ll use ProGet as an example, but the concepts are similar in other tools.

Configuring your NuGet Feed for Symbol Serving

ProGet has an integrated Symbol Server on its NuGet feeds. You can select the type of Symbol Packages that will be supported by the Symbol Server:

Once enabled, you’ll be presented with a URL that you can use to configure Visual Studio:

Verifying Indexed Symbols

After enabling the Symbol Server, you’ll want to verify that Symbol files were properly indexed. You can find this on the Symbols tab of a NuGet package in ProGet:

This will show you the unique identifiers (Id and Age) that Visual Studio uses to retrieve a PDB file.

Configuring Visual Studio to work with a Symbol Server

To debug in NuGet package libraries, Visual Studio must be configured to use the symbol server. This is where that earlier URL came in.

In Visual Studio, select Debug > Options… from the menu bar, then navigate to Debugging > Symbols from the tree menu. Then the symbol server URL you found earlier on the Manage Feed page and specify a Symbol Cache Directory.

By default Visual Studio will use %LOCALAPPDATA%\Temp\SymbolCache, but you may specify any path.

And that’s it! From a developer perspective, it’s about as easy as configuring a private NuGet feed in Visual Studio.

Alternatively: Debugging NuGet Packages Without Symbols

While NuGet Symbol Packages provide an efficient integrated debugging & development solution, something that I refer to as “munging” project files is an alternative, relatively straightforward solution.

“Munging” project files involves temporarily incorporating the code from the library project into the application you intend to debug. This solution is especially useful for situations where you want to write or edit the library code.

Munging project files can be done in a few simple steps. For this example, let’s say I need to debug/edit the Inedo.ExecutionEngine library, but I want to do so while using it in an application Otter.

Step 1: Uninstall the Packages Reference

In the Otter solution, Inedo.ExecutionEngine was installed in three different projects, so I right-clicked on my Solution, then “Manage NuGet Packages”, selected the library, and uninstalled it from the projects.

Step 2: Add Library Project

I right-clicked on my Solution, and selected Add Existing Project.

This image has an empty alt attribute; its file name is Munge2.png

After adding the project to the solution, it will show up at the bottom like this:

Step 3: Add Project References

On each of the three projects, I uninstalled Inedo.ExecutionEngine project reference, I added a project reference to my newly added library project (Inedo.ExecutionEngine).

Step 4: Debug & Edit Code As Normal

And that’s that! Inedo.ExecutionEngine can just be debugged and edited like all other projects on my solution! Note that you won’t be able to commit source control changes to the munged library in the same instance of Visual Studio. But you can always commit your library changes later. 

Step 5: Undo Project “Munging”

Once you’re done, make sure to undo the project “munging” to your library.  Accidentally committing these changes will lead to broken builds and a headache for the rest of your team.

Project Munging with Tools & PowerShell

If you don’t do this very often, it’s not that big of a deal to click this many times. However, if the multiple steps and clicks are a little too bothersome there is a Visual Studio Extension that can do some of the work, but it’s outdated and doesn’t work on newer project types.

It’s really simple to just write a very basic PowerShell script that uses the dotnet CLI.

# Munge-InedoLib.ps1 

$pkgName = "Inedo.ExecutionEngine"
$pkgProjFile= "C:\Projects\Inedo.ExecutionEngine\Inedo.ExecutionEngine\Inedo.ExecutionEngine.csproj"

$slnFile = "C:\Projects\Otter\src\Otter.sln" 
$projFilesToMunge = @( `
   "C:\Projects\Otter\src\Otter.Service\Otter.Service.csproj", `
   "C:\Projects\Otter\src\Otter.WebApplication\Otter.WebApplication.csproj", `
   "C:\Projects\Otter\src\OtterCoreEx\OtterCoreEx.csproj" ` 

dotnet sln $slnFile add $pkgProjFile 
foreach ($projFile in $projFilesToMunge) {
   dotnet remove $projFile package $pkgName
   dotnet add $projFile reference $libProjFile 

Debug NuGet Packages With ProGet

Debugging NuGet packages is anything but intuitive. Using the solutions outlined in this article will allow your developers to more easily integrate debugging with NuGet.

We recommend using NuGet Symbol packages, especially if you’re debugging into library code. However “Project Munging” is a fine alternative as well. ProGet supports these solutions so your developers can debug NuGet packages as painlessly as possible.

Debugging is important, but there’s a lot more to learn about NuGet packages to successfully use them on the Enterprise level. Sign up for our mailing list to receive our upcoming guide that will guide you through everything NuGet!

Eric Seng

Eric Seng