At Smokeball we design our software using a service-oriented architecture which means our application projects tend to be no more than a shell that reference a bunch of internal NuGet packages.

Different teams are responsible for each of these NuGet packages (and there are a lot of them!) so including the projects in the main Xamarin application solution isn't an option. Unfortunately, this can make debugging and stepping through these internal components much harder when they're added via NuGet packages.

While SourceLink can help us debug and step in to these components in our desktop applications, it isn’t currently supported in Xamarin.

But there is another way! Here is how we build our NuGet packages so that developers can easily debug them if needed from the main application. This method will work for internal NuGet packages within an organisation - it won't work for third party NuGet packages (unless they follow this method too!)

Ultimately this method boils down to 2 basic concepts

  • Include the .pdb file in the NuGet package
  • Ensure the source file locations baked in to the .pdb are mapped to a well-known location

Sounds simple right? However, there are a few gotchas to keep in mind.

Let's start with a .NET Standard 2.0 class library for our Reminders component that we'll want to include in our Xamarin app.

The new SDK-style projects have made it trivial to create a NuGet package from a project using either Visual Studio or the donet cli.

dotnet pack ItOps.Reminders.Xamarin.csproj

But the resulting NuGet package (.nupkg) will only contain the ItOps.Reminders.Xamarin.dll, not the .pdb.

If we were to pass in the --include-symbols to the pack command above, it would generate a separate *.symbols.nupkg that contains the .pdb file we need for debugging.

However, we can get a better debugging experience by including it in the main package. According to Microsoft:

Embedding symbol files in the main NuGet package gives developers a better debugging experience by default. They don't need to find and configure the NuGet symbol server in their IDE to get symbol files.
The downside to embedded symbol files is they increase the package size by about 30% for .NET libraries compiled using SDK-style projects. If package size is a concern, you should publish symbols in a symbol package instead.

In order to get all the files we need in the main NuGet package, we can add this property to our .csproj file.  

AllowedOutputExtensionsInPackageBuildOutputFolder
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup> 
      <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb;.xml;</AllowedOutputExtensionsInPackageBuildOutputFolder>
  </PropertyGroup>
    <!-- Other properties ommitted -->
</Project>

Now when we run dotnet pack the resulting NuGet package will contain our assembly and the .pdb file. Success!

This NuGet package can be referenced in our main Xamarin application and we can now step in to the Reminders code. (Make sure the Just My Code setting is disabled in Visual Studio)

While this works for a NuGet package built and debugged locally on the same machine, chances are that we won't be able to debug a NuGet package built on our Continuous Integration build server.

The reason for this is that when a .pdb file is generated, it embeds the location of the source code.  Because each developer (and build agent) checks out the source code to a different location, unless our code is also in the same location Visual Studio won't be able to locate the source when debugging.

We can rewrite the source code location inside the pdb while it is being generated by making use of the PathMap property in our .csproj file.

<PathMap>$(MSBuildProjectDirectory)=C:\repos\itops\reminders\xamarin</PathMap

In this case we are mapping the current project build directory (which could be a random folder on the build agent) to a well-known location.

Developers can then either move their local copy of the source code to C:\repos OR create a directory junction to map this path to the actual location of their repository (e.g. D:\code).

mklink /J D:\code C:\repos

With all this in place, we can now debug in to NuGet packages built locally and on the build server.

One last bit to tidy up. Rather than having to modify all our .csproj files individually to set these properties, we can take advantage of a single Directory.Build.props file.

By checking in this file to the root of our git repository, it will automatically apply these settings to every project in the repository.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb;.xml</AllowedOutputExtensionsInPackageBuildOutputFolder>
    <PathMap>$(MSBuildThisFileDirectory)=C:\repos\itops</PathMap>
      
    <!-- Additional properties that we'd like to apply repository wide -->
    <Company>Smokeball</Company>
    <Authors>Smokeball</Authors>
    <DebugSymbols>true</DebugSymbols>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>
</Project>

Once Xamarin finally supports SourceLink we will be able to do away with this method and Visual Studio will be able to pull the source code directly from our remote git repository.

Additional notes

To inspect the file paths embedded in the pdb you can use the Dia2Dump tool for Windows pdb files (DebugType = Full)

Dia2Dump.exe ItOps.Reminders.Xamarin.pdb

or for a portable .pdb (DebugType = Portable) you can view the file paths by opening it in Notepad++.

One last thing - if you use ProGet to host your NuGet feeds, then ensure the option to strip symbol files from packages is NOT selected.