Seamless Debugging of NuGet Packages

Sharing and reusing code between developers and teams has always been a serious challenge. Done badly it may gravely impede development and result with mediocre quality. Sharing source code per se has lots of problems on its own. Therefore, virtually every technology stack has a way of distributing versioned binaries instead. NuGet is the standard for sharing binaries in the .NET world. In open-source communities it’s natural to be able to view source code of a binary package. The next step is an ability to debug third-party binaries if debugging symbols are present. This, however, requires the source code to be downloaded and the debugger to be pointed to it. Source Link is an attempt to make the whole experience seamless. Essentially, it works by enriching packages' metadata with source control specific information which then can be used on debug time to automatically download and display required source code.

A need of a private NuGet server comes in a growing company sooner or later. People realise that reusable components (like internal frameworks, templates etc.) must be extracted, packaged, versioned, and distributed. However, the main disadvantage of this approach is that developers lose ability to easily debug such a code. Publishing pdbs and using Source Link brings that seamless experience back.

After a rather disappointing experience with NexusOss I found this open-source pearl BaGet - a NuGet and symbol server. It might not have all the bells and whistles needed for a big enterprise (yet). It should be sufficient for a medium team though. The main selling points are:

  • It’s .NET Core - runs in Docker
  • Supports NuGet V3 protocol
  • Can act as a transparent proxy/cache for nuget.org
  • Can act as a symbol server - supports snupkg
  • Has a familiar UI (resembling the one of nuget.org)
  • Easy to set up

Having a NuGet server with symbols support and some code in a hosted GitLab instance I was able to put together a rough tutorial that should help you achieve the first-class debugging experience for NuGet packages.

The following properties must be set in csproj files or passed to dotnet pack via cmd line:

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

With the above additions dotnet pack automatically creates two packages - the typical .nupkg and the new symbol package .snupkg. The symbol package is automatically discovered by dotnet nuget push and pushed to the nuget server:

info : Pushing Xxxxx.0.0.2.nupkg to 'https://nuget.local/api/v2/package'...
info :   PUT https://nuget.local/api/v2/package/
info :   Created https://nuget.local/api/v2/package/ 1406ms
info : Your package was pushed.
info : Pushing Xxxxx.0.0.2.snupkg to 'https://nuget.local/api/v2/symbol'...
info :   PUT https://nuget.local/api/v2/symbol/
info :   Created https://nuget.local/api/v2/symbol/ 58ms
info : Your package was pushed.

Include the following property into csproj.

  <PropertyGroup>
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
  </PropertyGroup>

You also need to add a Source Link nuget specific to your version control. For GitLab it’s the following:

dotnet add package Microsoft.SourceLink.GitLab --version 1.0.0-beta2-19367-01

If you’re using Docker as your build environment you need to make sure

  • .git folder is copied into the container (or at least certain files)
  • the relative paths of source files in the container match relative paths in the repository

Assuming your file is under /path/to/my/File.cs relative to the repository root and the path in the container is /your-work-dir/path/to/my/File.cs the .git directory should be copied to /your-work-dir/.

For quick testing without Visual Studio (or as a part of CI) you can use dotnet tool install --global sourcelink. It has a few modes but essentially it can verify the correctness of resolved URLs.

There is an important unresolved issue (at least for the GitLab implementation) - Source Link does not work with GitLab private repositories. Discussions here, here, and here.

Just add the following snippet to your current debug configuration in launch.json.

{
  "justMyCode": false,
  "suppressJITOptimizations": true,
  "symbolOptions": {
      "searchPaths": [
          "https://nuget.local/api/download/symbols"
      ],
      "moduleFilter": {
          "mode": "loadOnlyIncluded",
          "includedModules": [ "YourCompany*.dll" ]
  }
}

VS is slightly more involved although configuration is essentially the same.

  • Check Options -> Debugging -> Enable Source Link support

  • Check Options -> Debugging -> Enable Source Link support -> Fall back to Git Credential Manager authentication for all Source Link requests

  • Options -> Debugging -> Symbols -> Symbol file (.pdb) locations

  • Assuming BaGet is hosted at https://nuget.local you need to add https://nuget.local/api/download/symbols.

  • Uncheck: Options -> Debugging -> General -> Enable Just My Code
  • Options -> Debugging -> Symbols -> Automatic symbol loading preference

  • Click Specify included modules and specify a filter for your modules, e.g. MyCompany.*.dll.

The initial set up may look quite involved. However, the benefits of seamless debugging are considerable. Developers being able to step into internal NuGets can get familiar with the lower-level code and can help finding bugs in there too. With third-party Source Link-enabled packages they also get exposed to a usually higher quality code they can learn from and may also be encouraged to get involved in open-source communities.