NuGet
How to Debug NuGet Packages with Symbols and Source Link Painlessly
This article is part of our series on NuGet at Scale, also available as a chapter in our free, downloadable eBook.
Debugging private NuGet packages is a serious headache. Honestly, it’s one of the biggest reasons dev teams hesitate to break up their monolithic .NET solutions. Unlike a regular referenced library project, you can’t easily step-into a NuGet package’s code and start debugging. And if you need to just tweak a method? Yep—time to rebuild the whole package. At scale, this slows down development and increases friction in the inner loop.
Luckily, there’s a simple solution to this problem: NuGet Symbol Packages. To make debugging your NuGet packages less of a nightmare, the real game-changer is setting up a Symbol Server, and optionally Source Link. With .snupkg files—or symbol-enabled .nupkg files— pushed to a symbol server, you can pull .pdb files and source maps, to step-through your package’s code in Visual Studio without the usual hurdles.
In this article, we’ll break down Symbols and Source Serving (Source Link) and how to create NuGet Symbol Packages with Source Link. We’ll also look at Legacy Symbol formats and packages and cover configuring Visual Studio to debug Symbol packages.
Demystifying Symbols & Source Serving (Source Link)
The secret sauce behind debugging NuGet packages? It’s all about Symbols and Source serving. There’s actually four different technology “layers” working together to make this happen.
But I won’t lie—it’s complicated. It took me a little while to wrap my head around how everything fits, especially after reading a bunch of outdated (and sometimes just plain wrong) articles. In any case, I’ll do my best to walk you through each piece and explain how they all connect!
What the heck are Symbols, anyway?
You’ve likely noticed the .pdb files in your bin directory after building your project—right next to your .dll or .exe files. These are your Symbol files, and they basically map compiled assemblies (i.e., the .dll and .exe files) to the original source code (i.e., the .cs files) that were used to build them.

Visual Studio uses these .pdb files to let you step-through your code while it’s running in debug mode on your machine.
But .pdb files are useful even outside of interactive debugging, providing way more context in stack traces when something crashes—like showing you the full disk path to the original source code, even when your application errors on a different machine.
What is a Symbol Server?
Fun fact👩🏫: Symbol files actually predate NuGet—and .NET as a whole—by at least a few decades. Designed in the era when disk space was at a premium, this meant Symbols were really shipped with software, which was great for saving space, but terrible for debugging.
That’s where Symbol Servers came in. When you build a library, the complier embeds a unique identifier (basically a GUID) into both the .dll and the .pdb files. If you upload the .pdb to a Symbol Server, tools like Visual Studio can later fetch it using that unique ID embedded in the .dll file.

That’s how a debugger like Visual Studio can load up Symbol files when your app crashes—you don’t need to have every single .pdb file sitting on your machine. Visual Studio just grabs the right one on demand from the Symbol Server, using that embedded ID.
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 creates a Symbol package with your normal NuGet packages 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
Running one of these commands creates MyPackage.nupkg and MyPackage.snupkg in your working directory, one with the .dll files and one with the .pdb files. Your regular .nupkg package can then be pushed to a NuGet feed, with your Symbols .snupkg package being pushed to a Symbol Server.
What is Source Link?
Remember how .pdb files map your compiled assemblies (.dll and .exe) back to the original source code (.cs files)? Well, that “map” only works if the debugger can find the exact version of the source code used when the assembly was built. 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 right into the package metadata. That way, when you’re debugging, Visual Studio knows where to locate the required code files.

Enabling Source Link in your own .NET project means setting a few properties and then adding a NuGet package specific to your NuGet repository (e.g. Microsoft.SourceLink.GitHub).
Legacy Symbol Format & Packages
Believe it or not, NuGet Symbol and Source Serving was even messier a few years back, and if you’re poking around older packages, you might run into some of those legacy formats, so I’ll give you a quick rundown to help them make sense.
Legacy Symbol Packages (.symbols.nupkg)
These days, .snupkg Symbol packages are the norm—but you might still come across the older .symbols.nupkg format. These legacy Symbol packages usually bundle up the .pdb files, source code, and the compiled .dlls… which defeats the point of separating things cleanly.
Microsoft PDB (Legacy Symbol Files)
Not all .pdb files are created equal. There are actually two kinds—Microsoft PDB (aka Windows PDB) and Portable PDB. They might look the same, but under the hood, they’re totally different file formats.
Microsoft PDBs go way back—like early ’90s old. They’ve been used for everything from native Windows drivers to classic .NET Framework assemblies, but it only works on Windows and was never intended with cross-platform (Linux) debugging in mind.
Portable PDBs, on the other hand, are a fresh rewrite from the .NET team. As the name suggests, they’re portable—so they work across platforms like Linux and Windows. The catch? They only work with .NET libraries, which means if you’re building native (C++) libraries for Windows, you’ll be sticking to the Microsoft PDB format.
These days, if you’re working with NuGet packages, .NET 9.0, or .NET Core or later, you likely won’t come across too many Microsoft PDB Symbol files.
Legacy Source Server
Before Source Link became the go-to for NuGet packages, an older system called Microsoft Source Server was used to pull the exact version of the source code used to build a library. These days, it’s mostly obsolete, built for a very different time when Git and web-based source control weren’t a thing. But if you’re using TFVC or SVN, it’s basically your only option.

How to Debug NuGet Packages with Visual Studio
So making NuGet Symbol packages is pretty easy, since you just specify the SymbolPackageFormat when running the nuget.exe or dotnet CLI.
dotnet pack MyPackage.csproj -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
From here, you just need to publish your .nupkg package to a private NuGet feed and your .snupkg to a Symbol server. Of course, you’ll need a capable private NuGet Server.
I’ll use ProGet to show how this works, 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, and you can choose the type of Symbol packages (modern and legacy) supported.

Once set up, you’ll get a Symbol Server URL to configure Visual Studio:

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

Now you can see the unique identifiers (ID and Age) Visual Studio uses to retrieve a .pdb file.
Configuring Visual Studio to work with a Symbol Server
To debug NuGet package libraries, Visual Studio needs to use the Symbol Server, which is where your Server URL from earlier comes in.
In Visual Studio, select Debug > Options… from the menu bar, then navigate to Debugging > Symbols from the tree menu. Then add (+) your ProGet symbol server URL 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! For devs, it’ll be as easy as configuring a private NuGet feed in Visual Studio.
Alternatively: Debugging NuGet Packages Without Symbols
While NuGet Symbol packages offer an efficient, integrated solution for debugging and development, something I call “munging” project files is a pretty simple alternative solution.
Munging project files means temporarily incorporating the code from the library project into the app you’re gonna debug, especially useful for situations where you want to write or edit the library code.
This can be done in a few simple steps, and I’ll show you an example where 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.

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 massive 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, a lot of steps and clicks become a little too bothersome, there’s 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 can help your developers to more easily integrate debugging with NuGet.
We recommend using NuGet Symbol packages, especially if you’re debugging library code. That said, project munging is a good alternative. ProGet supports these solutions so your developers can debug NuGet packages as painlessly as possible.
There’s a lot of useful information here! Be sure to save or bookmark it for future reference! Better yet, why not download our eBook, “NuGet at Scale”? It covers everything we just talked about, plus it dives deeper into NuGet servers, filtering unwanted packages, and more! Download your free copy now!