Debugging NuGet Packages
Have you ever noticed that now we have embraced the micro-services architecture of doing things, instead of one big monolithic repository of code we have it all broken into smaller repositories of single/reduced responsibility...
To share these little bits of wisdom we package them up into little parcels that we can import into 100s of other projects distributed across our services. Then, something goes wrong and we can't debug what is going on inside these little gems and we wonder why we did this and consider if a monolithic repository was all that bad after all.
Yeah, me too.
So what can we do about it? Well I've picked up a few tricks, forgotten a few, and relearned them again once more. So I am going to note them down now for my future-self.
Before we talk about debugging NuGet packages we need to talk about what we need to actually debug our code and how the debugger uses the various assets we have available to help us do that. The following items are pretty well common for most compiled languages on windows and linux platforms.
- Our source-code: this is the stuff we write and where we are going to make changes should we find issues;
- The compiled execution: this is the thing we build and execute, usually an assembly;
- A map-file, this allows us to identify where in our source-code the execution is currently at when debugging.
For .NET this looks like the following
|C#, F#, VB.NET, ...||DLL, EXE||PDB, MDB|
Now when we are debugging locally we have all these things but when we download a NuGet package it is unusual to find a PDB in the package, probably due to size, and it's highly unlikely that we'll see the source code packaged.
Debugging NuGet packages
If you're making your own NuGet packages then please consider generating and including the PDB files with your compiled assets. It will make your life so, so much easier because you'll have better quality stacktraces should an error occur and you can quickly locate where an issue happened, hopefully avoiding the need to debug in the first place.
Having the location of the errors in your stacktrace saves so much time in identifying where to look for the error from which you can then trace the cause.
System.InvalidOperationException: L'opération n'est pas valide en raison de l'état actuel de l'objet. File "C:\projects\opencover\main\OpenCover.Framework\Communication\MessageHandler.cs", line 113, col 21, in StandardMessage Int32 StandardMessage(OpenCover.Framework.Communication.MSG_Type, OpenCover.Framework.Manager.IManagedCommunicationBlock, System.Action`2[System.Int32,OpenCover.Framework.Manager.IManagedCommunicationBlock], System.Action`1[OpenCover.Framework.Manager.M... File "C:\projects\opencover\main\OpenCover.Framework\Communication\CommunicationManager.cs", line 60, col 13, in HandleCommunicationBlock ...
Before you ask, yes, you can generate PDB files for release builds of your assemblies; also please consider including any intellisense files in your NuGet packages if you have generated them but only if they are suitable for public consumption.
Even with the PDB files available you are not going to be able to debug very well, if at all, unless you have access to the source code. You may be lucky as you may already have it downloaded (or cloned) or you can pull it from the relevant source control system. Even then you will still need to make sure your source code matches the code base at the time the assembly was built and that can be a little tricky at times as you'll need to know when it was built and revert your code back to that point in-time.
If you are using .NET and have the right tooling there is a simpler way. When using Visual Studio with ReSharper it will simply allow you to debug any assembly without source code as long as the PDB file exists. You can either step right into the method you want to investigate and ReSharper will automatically create the source code for you by using the IL and creating equivalent source code. You can also use the
Go To Implementation shortcut (usually Ctrl+F12) and start drilling into the source code of the 3rd party assembly placing breakpoints as you go before you start your debugging run.
What happens when you don't have a PDB? Well, you can still use the
Go To Implementation shortcut. However whilst debugging, when you try to create a breakpoint, it will initially fail but if the PDB can be located i.e. in a symbol store, or it can be generated dynamically, it will prompt you to enable debugging and then you can continue debugging as before; generated PDBs are, by default, placed in
The one disadvantage of this approach is that the source code you are debugging is not your actual source code but it is a close representation of what it could be i.e. source code that when compiled should produce the same IL.
I also understand not everyone can afford ReSharper but all is not lost because the above functionality is also available as part of DotPeek which is free.
Is there another way?
There does appear to be a solution on the horizon (still in Beta at the time of writing but has strong support) that will allow debugging of packages and it is called "Source Link", it can be used with most git repositories such as GitHub, Bitbucket and Azure Devops etc and what it does is add a special link into your nuget package to your git repository along with the appropriate SHA, with this information and a supporting debugger (i.e. Visual Studio) can pull the appropriate source code resources. As I said it is still beta but if you are okay to update all your packages going forward then this is probably the future.
Over time I can see that the latest packages on NuGet will start to use this method as it makes sense in that it resolves the issue of stepping through the actual code rather than a close representation and by using the SHA it focuses on the files as they were at the time. Here is a demo of Source Link being used in Newtonsoft.Json.
What if I have no access to the source code?
You can still create a PDB for your assembly using tools that come with Visual Studio and you can access from your developer command prompt e.g.
ildasm /out=target_assembly_source.il target_assembly.dll ilasm target_assembly_source.il /dll /pdb
Once you have your new DLL and associated PDB you will now need to recompile using the newly rebuilt assembly, then you'll be able to step into the code, you will be stepping though the generated IL code though, this isn't perfect though and YMMV.
As always, your feedback is appreciated.