Have you encountered a case where you needed to debug an external library? Be it Newtonsoft.Json or AspNetCore MVC, sometimes things go wrong and you need to get to know package internals to find the issue. The same often applies to packages from your organization private feed.

The issue is, debugging them is not as easy as debugging projects living within one solution. After all, many times all you have is a DLL file. Sometimes, if you have access to the source code, you can just switch DLL reference to csproj reference and debug this way. But, not always you do have access to the sources. And even if you do, you may aim for a better debugging experience, where sources get downloaded automatically when needed by debugger.

Turns out, there are lots of handy ways to debug such libraries straight from Visual Studio!

First, you need PDBs

The most important thing, when trying to debug anything is to obtain PDB files for the DLL. These files act as a bridge between instructions loaded into memory and the source code. They store symbol information, like variable names and functions, linking them to the source files.

So, a PDB might encode information such as “variable X is defined in file Helpers.cs line 32”. Hence, you see how crucial these files are for the debugger. Without them, we wouldn’t have any idea on how to link running executable with cs files.

Because of their close connection with sources, each build results in different symbol files. Change a single line in the code, and new PDB is needed as the location of symbols might have shifted. Actually, even without introducing changes to the code base you might still need to regenerate them. Why ? Because the compiler might produce slightly different IL assembly code.

So, always make sure you have relevant PDB files for the exact version you’d like to debug. If there is a mismatch, it probably won’t work.

Where to get PDB files from?

Hopefully, symbols for most nuget.org and microsoft packages are distributed through public symbol servers. They are the go-to place to find PDBs of interest. Visual studio integrates nicely with them, so if you just enable it symbols get downloaded on the fly when needed.

Go to Tools -> Options in VS, then Debugging -> Symbols to reveal symbol servers options

Symbol Server Settings

Also, you might want to bring up Debug -> Windows -> Modules window. It’ll show you modules currently loaded into memory and the information on whether or not symbols are available for them. It might take a while to load symbols for the first time. On subsequent debugging sessions, they should be loaded from cache so it won’t be such a big issue.

Modules Window

Moreover, you can generate PDBs on the fly with Resharper in case symbols are not available in any of your configured sources. This is really handy for dealing with closed source libraries, as they might not ship symbols.

Then, you need the sources

Besides symbols, you also need the source files that symbols link against. One way is to just clone the sources from repository and checkout appropiate commit. For public packages, you might want to look at releases to find the commit in question.

Assuming you have symbols in place, VS will prompt you to supply source files when needed. It’s enough to select just one file, it’ll find the other ones within this directory on its own.

Alternatively, you may also edit these paths by right clicking solution -> Properties -> Common Properties -> Debug Source Files Debug Source Files

While this works, it sounds a bit tedious, doesn’t it?

Lucky for us, nowadays most libraries on nuget.org have SourceLink integrated. It embeds some metadata in the nuspec itself, allowing Visual Studio to download sources automatically for you. It’ll checkout the very exact commit specified in nuspec. Combined with symbol servers, that’s a Plug & play debugging experience!

Then, there is the third way, in case you don’t have access to PDBs. You can use ReSharper to generate them on the fly and then keep debugging with decompiled sources. While this sounds totally awesome, have in mind that you are dealing with a lossy backwards translation from the binary. So, there might be glitches :)

Set up Visual Studio

Having the symbols and sources in place, there are still few Visual Studio quirks you need to have in mind. Without properly checking some checkboxes in the settings, debugging won’t work.

Let’s briefly look at those by going to Tools -> Options -> Debugging. I highlighted the interesting ones in red:

Modules Window

  • Enable Just My Code
    This one is important. Basically, you need to have it unchecked if you want to debug external libraries. Otherwise, the debugger is just going to stick to your solution
  • Enable .NET Framework source stepping
    This will allow you to debug .NET Framework sources. That is, only if the particular symbols and sources have been published to the .NET framework reference source
  • Enable source server support
    Yet another checkbox for debugging .NET Framework. If you want to download sources from the reference source, you need to have it checked.
  • Enable Source Link support
    This enables downloading sources automatically, if the package uses SourceLink integration. It can also be used with private feeds. In my opinion, you should always have it checked. Anyone knows, why it’s not the default setting by now?
  • Require source files to exactly match original version
    So, if your sources don’t exactly match PDB files but you still would like to give it a go - check this box. Of course, glitches can happen when you enable this. Also, for some reason, it’s required to have it checked when debugging .NET Framework

How to distribute your own symbols?

Now that we know how to use sourcelink and symbol servers, let’s briefly cover how you can use them with your own private packages. Your team is going to thank you for shipping symbols some day, trust me on that. Also, it doesn’t take much effort to have your libraries properly setup for debugging. For example, you can:

1. Place symbols in the same package

That’s the easiest way and to be honest, I think it’ll be just enough for most of the teams. Just place PDBs next to your DLLs and you are good to go, the debugger will find them, as they live in the same directory as binaries. In dotnet core you can use dotnet pack --include-symbols to generate such package. This is going to create yourpackage.sumbols.nupkg file containing both binaries and the symbols. Just push it as usual to the feed and you are good to go.

However, this has a drawback of increasing the package size. Symbols may account for rougly 30% of the package size, as per msdn.

2. Generate snupkg

Some time ago, a new way got introduced. That is, you can now distribute symbols with a new format called snupkg. You can push it to the feed the same way as you’d with a “normal” package. However, a compatibile feed will understand this special extension and publish it to the symbols server instead.

This works on NuGet.org , I’m not sure about other feeds though. E.g. it’s not supported in Azure DevOps and ProGet at the moment, so if there are any that you are aware of - let me know.

Anyway, to have snupkg generated on dotnet pack, just place following snippet in your csproj

<PropertyGroup>
    <IncludeSymbols>true</IncludeSymbols>	
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>	
</PropertyGroup>

3. Check your CI tooling

Most probabbly, your CI tool already has support for shipping symbols and hosting symbol servers. For example, in Azure Devops there is a dedicated task for doing so. Or, in TeamCity there is a plugin available.

Having the symbols shipped, it’s also a good idea to set up sourcelink. This allows Visual Studio to automatically download sources, even from private repositories. Assuming you have symbols shipped as well, and the IDE is set up correctly, this makes debugging work out of the box. Setting it up is relatively simple. It comes down to modifying csproj a bit:

<!-- taken from https://github.com/dotnet/sourcelink -->
<Project Sdk="Microsoft.NET.Sdk">
 <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
 
    <!-- Optional: Publish the repository URL in the built .nupkg (in the NuSpec <Repository> element) -->
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
 
    <!-- Optional: Embed source files that are not tracked by the source control manager in the PDB -->
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
  
    <!-- Optional: Build symbol package (.snupkg) to distribute the PDB containing Source Link -->
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  </PropertyGroup>
</Project>

And adding nuget package specific for your provider, e.g. GitHub, GitLab etc. For example:

<ItemGroup>
  <PackageReference Include="Microsoft.SourceLink.AzureRepos.Git" Version="1.0.0-beta2-19554-01" PrivateAssets="All"/>
</ItemGroup>

You can find all providers, along with excellent documentation on the SourceLink GitHub.

Summary

I wanted this article to be a handy cheatsheet on debugging .NET applications. So that, next time you or I are stuck with external dependency not working as expected, you can pick it up and set up all the necessary things quickly.

Also, I wanted to stress out that shipping symbols along with your package is a really good practice. Everyone will thank you for doing this, as there is nothing more frustrating than being stuck while debugging. Lucky for us, it’s not hard at all to place necessary metadata in the package. It just takes few lines of XML, so there is really no reason not to do it.