Easily making internal libraries debuggable in .NET
At NewDay, we deeply care about software testing and, while we are big fans of TDD, there is no denying that being able to debug software effectively is a crucial aspect of software engineering.
Favouring a debugging session instead of writing a new unit test is a big topic on its own, so here we will only focus on a specific aspect: being able to step into an internal library from a consuming application or service in .NET.
.NET has a long history of providing a fully featured debugger via Visual Studio. When working with Microsoft libraries or many open-source ones, we get the ability to step into the code, how is that possible? And how we can achieve the same behaviour for our company internal libraries?
Leveraging debug symbols
In general, to debug a compiled language, on top of having access to the source code, you need the debug symbols, which map the binary to the original source code. In .NET, this role is fulfilled by the Portable Database (or PDB).
For Microsoft libraries or many open-source ones, the PDBs are automatically retrieved by Visual Studio from the Microsoft public symbols server or from the NuGet.org symbol packages (.snupkg) feed, respectively.
When working in an enterprise scenario, we don’t usually have either of those mechanism available for our internal libraries: symbols servers have never been popular and are tricky to setup. On the other hand, while snupkg are the standard for modern .NET libraries, they are not supported by the most popular software used to host internal package feeds, like JFrog Artifactory or GitHub Packages registry. For this reason, the best approach is to include the PDBs as part of our actual library NuGet package.
Including PDBs in a NuGet package
The common solution to provide debug symbols for private packages used to be to include the PDBs in the NuGet packages alongside the dll.
For example, in early versions of .NET Core you could add a, admittedly quite hard to remember, line to your csproj
to achieve this.
Unfortunately, with changes in .NET Core 3.0 to the project system, it is no longer possible to consume the PDB files included in NuGet packages.
Luckly, we can rely on another mechanism that, while deeply different in nature, allow us to achieve the same result: embedding the PDB inside the DLL. It has a few drawbacks, like having larger packages and not being able to easily strip out PDBs from deployables, however these problems are rarely relevant for internal libraries, especially when used in internal microservices and APIs.
In current .NET versions, this is easily achievable by adding the <DebugType>embedded</DebugType>
property to your library csproj
, which is much easier to remember than the previous technique and one day it could become the default behaviour.
There is a final aspect that we need to mention when talking about including a PDB: making sure it is deterministic. While .NET builds have become deterministic by default in .NET, ensuring the same DLL is produced no matter where it is built, to have PDBs with normalized paths to our source file, we also need to add the <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
property, usually with a conditional to only apply the normalization when building in CI, but we will look into an easy way to achieve all these later on.
Getting the source code
Now that our library contains the required metadata to map the DLL to the source code, we need to be able to easily retrieve it. Nowadays we all use version control systems, so the easier way is to retrieve it directly from it. The .NET platform provides Source Link, a nifty tool integrated into Visual Studio, which provides the ability to automatically include metadata into NuGet packages that point to the source code hosted in our version control hosting platform of choice from which Visual Studio can then automatically download it when stepping into the code. Thanks to the support to multiple authentication mechanism as well as on-premises solutions like GitHub Enterprise, this works well in the enterprise scenario, allowing us to leverage the same tool as the opensource community without extra configuration, as easy as adding a package reference and a couple of project properties. But can it be even easier to integrate it?
Putting it all together
As we had seen, by putting a few pieces together and using the right configuration, we can provide a brilliant debugging experience for our internal libraries which has nothing to envy to opensource or Microsoft libraries. Of course, we would not want to have to replicate a bunch of settings across every project and, while creating shared props
or targets
is an option, there is no need for it.
In fact, there is a .NET Foundation’s package called DotNet.ReproducibleBuilds that takes care of all these steps necessary to get Source Link fully working and CI builds deterministic, including logic to detect your correct CI server. All you need to do is to add this package either to your csproj, or more convenient if you have multiple projects published as library in the same solution, to your Directory.Build.props
` file.
Investigating Source Link issues
If you are having issues in navigating to the source code, you should check the logs in Navigate to External Sources in the Visual Studio’s Output View.
Also make sure you have Enable Source Link support enabled and consider enabling the integration with Git Credential Manager if you experience authentication issues.
Furthermore, don’t forget that to be able to step into libraries, apart from thrown exceptions, you need to disable Just My Code and set the Symbols loading rules appropriately.
Extra goodies
In recent versions of Visual Studio 2022 there is now the ability to use “Go To Definition” on references of our internal libraries and magically go to the source code relying on the same mechanism previously described used during debugging. While the feature is still not perfect, it works in most scenarios, and it keeps getting refined with each feature update!
So easy!
Thanks to the ability of the IDE to automatically retrieve debug symbols and the source code, allowing both to “step in” during debug as well as using “Go To Definition” when navigating the codebase, working with private libraries has never been so easy!