NuGet SourceLink and Symbol Packages: Step-Through Debugging for .NET Libraries
You're debugging a production issue. You've traced the problem to a library your team ships -- something consumers install as a NuGet package. You set a breakpoint, hit F5, and Visual Studio steps right over the library call like it doesn't exist. No source. No symbols. Just a hollow stack frame.
That's the gap nuget sourcelink and nuget symbol packages are designed to close. With the right configuration, consumers of your library can step through your source code in their debugger, exactly as if they had your repo cloned locally. No manual setup on their end beyond flipping one Visual Studio setting. This article walks through how to configure nuget sourcelink -- from the first MSBuild property to pushing your .snupkg file to NuGet.org.
Whether you're shipping an open source library or an internal package to other teams, enabling step-through debugging is a professional quality-of-life feature that separates polished packages from frustrating black boxes.
What Is SourceLink?
SourceLink is a set of MSBuild tooling that embeds source file URLs into your PDB (Program Database) file when you build your library. The nuget sourcelink feature does not change your compiled DLL -- it enriches the debug symbols that ship alongside it. Instead of pointing to file paths on your local machine, the PDB now references the exact commit and file path in your remote git repository.
When a consumer's debugger loads your library, it reads those embedded URLs and downloads the original source file on the fly. If you push v1.2.3 of your library, the PDB references the exact git commit tagged as v1.2.3. The consumer steps through the code exactly as it was at that version -- no drift, no guesswork.
SourceLink is not a single package. It's a family of packages, each targeting a specific git hosting provider:
Microsoft.SourceLink.GitHub-- for GitHub-hosted repositoriesMicrosoft.SourceLink.AzureRepos.Git-- for Azure DevOps (cloud / Azure Repos)Microsoft.SourceLink.AzureDevOpsServer.Git-- for on-premises Azure DevOps ServerMicrosoft.SourceLink.GitLab-- for GitLabMicrosoft.SourceLink.Bitbucket-- for Bitbucket
The provider package inspects your repo's remote URL at build time and generates the correct source file mappings automatically. You add one package reference and a few MSBuild properties. That's the entire author-side setup for nuget sourcelink.
What Are Symbol Packages (.snupkg)?
A NuGet package (.nupkg) contains your compiled DLL and metadata. It does not normally contain debug symbols. PDB files are separate artifacts.
A symbol package (.snupkg) is a companion package that contains only PDB files. NuGet.org operates a dedicated symbol server. When you push a .snupkg, the symbol server stores it indexed by the package identity. When a consumer's debugger requests symbols for your library, it queries the symbol server, downloads your PDB, and uses those SourceLink-embedded URLs to fetch the source.
The flow looks like this:
- You pack with symbols enabled -- a
.nupkgand a.snupkgare created. - You push both to NuGet.org. The
.nupkggoes to the package feed. The.snupkggoes to the symbol server. - Consumer installs your package and opens their debugger.
- Debugger queries
https://symbols.nuget.org/download/symbolsfor your PDB. - Debugger fetches source from your GitHub repo using the URLs embedded in the PDB.
- Consumer steps through your code.
Alternatively, you can skip the separate .snupkg entirely and embed the PDB directly in your DLL. More on that tradeoff in a later section.
Adding Microsoft.SourceLink.GitHub
For a GitHub-hosted repository, adding nuget sourcelink support requires one package reference in your .csproj:
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
The PrivateAssets="All" attribute is important. It marks this package as a build-time tool that should not become a transitive dependency for consumers of your library. They don't need it; you do.
Add this to the library project itself -- not a test project, not a solution-level file. The nuget sourcelink package hooks into the build process for that specific project.
Configuring SourceLink in Your .csproj
Here is a complete .csproj that combines nuget sourcelink with symbol package generation:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- NuGet package identity -->
<PackageId>MyCompany.MyLibrary</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Description>A reusable .NET library with full SourceLink support.</Description>
<!-- Repository info -- required for SourceLink to generate URLs -->
<RepositoryUrl>https://github.com/yourorg/yourrepo</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<!-- Embed source files not tracked by git (e.g., generated files) -->
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<!-- Use portable PDB -- required for .snupkg symbol package approach -->
<DebugType>portable</DebugType>
<!-- Enable symbol package generation -->
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<!-- Deterministic builds improve reproducibility -->
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
<ItemGroup>
<!-- SourceLink for GitHub -- PrivateAssets keeps this out of consumer's dependency graph -->
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>
</Project>
A few things worth highlighting here.
PublishRepositoryUrl tells the build tooling to include the repository URL in the resulting package metadata. Without this, the URL is used for SourceLink mapping but doesn't appear on the NuGet.org package page.
EmbedUntrackedSources handles an edge case -- if your build generates source files (T4 templates, Roslyn source generators, etc.) that git doesn't track, this property embeds their content directly in the PDB so consumers can still see them. For most libraries this is a safety net rather than a daily requirement.
Deterministic and ContinuousIntegrationBuild are nuget sourcelink's best friends. Deterministic builds ensure the binary output is identical across machines for the same input, and SourceLink documentation recommends ContinuousIntegrationBuild be true in CI environments because it enforces path normalization.
Choosing Between Embedded PDB and .snupkg
This is the most consequential decision in your nuget sourcelink setup. You have two paths:
Embedded PDB (<DebugType>embedded</DebugType>)
The PDB is folded directly into the DLL. One file. No companion package required. Consumers get symbols automatically when they install your package -- no symbol server configuration on their end.
The tradeoff is size. Embedding a PDB can add 20-200% to your DLL's binary size, depending on the codebase. For libraries with a large API surface, this matters. For smaller utility libraries, it's often the right choice.
Portable PDB + .snupkg (<DebugType>portable</DebugType> with <IncludeSymbols>true</IncludeSymbols> and <SymbolPackageFormat>snupkg</SymbolPackageFormat>)
The PDB lives in a separate .snupkg file, uploaded to the NuGet symbol server. The main .nupkg stays lean. Consumers who want symbols configure their debugger to point at https://symbols.nuget.org/download/symbols and symbols are downloaded on demand.
The tradeoff is friction. Consumers need to enable the NuGet symbol server in their debugger settings. Most developers who actively debug third-party code are used to this. But consumers who don't know about symbol servers will see the same black-box behavior they saw before.
Which should you choose? For open source libraries where debuggability is a selling point, embedded PDB is the friendlier nuget sourcelink option. For large libraries where binary size matters, or for internal enterprise packages where you can mandate the VS debugger configuration, .snupkg is the cleaner approach.
Generating and Pushing Symbol Packages
With the .csproj configured above, a single dotnet pack command produces both artifacts:
dotnet pack src/MyCompany.MyLibrary/MyCompany.MyLibrary.csproj
--configuration Release
--output ./artifacts
After this runs, you'll see two files in ./artifacts:
artifacts/
MyCompany.MyLibrary.1.0.0.nupkg
MyCompany.MyLibrary.1.0.0.snupkg
Pushing to NuGet.org requires two separate commands -- one for the main package and one for the nuget symbol package:
dotnet nuget push ./artifacts/MyCompany.MyLibrary.1.0.0.nupkg
--api-key YOUR_API_KEY
--source https://api.nuget.org/v3/index.json
dotnet nuget push ./artifacts/MyCompany.MyLibrary.1.0.0.snupkg
--api-key YOUR_API_KEY
--source https://api.nuget.org/v3/index.json
NuGet.org automatically routes .snupkg files to the symbol server (https://symbols.nuget.org/download/symbols) even when the --source is the main API endpoint. You don't need a different URL for symbols when pushing through the CLI.
If you want to push all .nupkg and .snupkg files from a build artifacts folder, you can use wildcards:
dotnet nuget push "./artifacts/*.nupkg" --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json
dotnet nuget push "./artifacts/*.snupkg" --api-key YOUR_API_KEY --source https://api.nuget.org/v3/index.json
Repeating this manually every release is a candidate for automation. If you want to remove the manual push steps entirely, automating NuGet publishing with GitHub Actions covers how to hook dotnet nuget push into a CI release workflow triggered by a tag push.
Enabling SourceLink in Visual Studio (Consumer Side)
If you pushed a .snupkg, consumers need to configure their Visual Studio to pull symbols from the NuGet.org symbol server. The default VS configuration doesn't include it.
Walk consumers through these steps:
- Open Tools → Options → Debugging → Symbols
- Click the + icon to add a symbol file location
- Enter
https://symbols.nuget.org/download/symbols - Enable the checkbox next to the newly added server
Then in Tools → Options → Debugging → General, make sure both of these are checked:
- Enable source server support
- Enable Source Link support
"Enable Source Link support" is what actually tells VS to follow the URLs in the PDB back to GitHub. Without it, symbols load but source navigation doesn't work.
A note on performance: the first time a consumer steps into a library method, VS downloads the PDB from the symbol server and caches it locally. Subsequent debugging sessions use the cache. The initial download can take a second or two on slower connections, but it's a one-time cost per version.
If your library ships embedded PDB instead of .snupkg, consumers only need to enable "Enable Source Link support". The symbol server configuration is not required because the PDB is already in the DLL they installed.
Verifying SourceLink Works
After building your library, you can verify the nuget sourcelink configuration is correct using the dotnet-sourcelink global tool. First, install it:
dotnet tool install --global dotnet-sourcelink
Then run the test against your compiled DLL:
dotnet sourcelink test ./src/MyCompany.MyLibrary/bin/Release/net8.0/MyCompany.MyLibrary.dll
A successful result looks like this:
sourcelink test passed: MyCompany.MyLibrary.dll
If the test fails, the output tells you which source files couldn't be mapped to a URL. Common causes are:
- The build ran against uncommitted local changes. Nuget sourcelink can't resolve URLs for files that aren't committed to the remote.
EmbedUntrackedSourcesis missing and your project has generated source files.- The
RepositoryUrlproperty points to a non-existent or private repo.
You can also inspect the embedded SourceLink JSON directly:
dotnet sourcelink print-urls ./src/MyCompany.MyLibrary/bin/Release/net8.0/MyCompany.MyLibrary.dll
This prints out every source file path alongside the URL SourceLink will use when a debugger requests it. It's a fast sanity check before pushing a release.
Adding this verification step to your CI pipeline catches SourceLink failures before they reach consumers. If you're already building your release pipeline in GitHub Actions, the dotnet sourcelink test command fits naturally into the verify stage before the push step. The article on automating NuGet publishing with GitHub Actions goes deeper on structuring that pipeline.
SourceLink for Other Git Providers
Microsoft.SourceLink.GitHub only works for GitHub-hosted repositories. Nuget sourcelink support for other hosting providers comes through separate packages -- swap in the one that matches your git host:
Azure DevOps (cloud / Azure Repos):
<PackageReference Include="Microsoft.SourceLink.AzureRepos.Git" Version="8.0.0" PrivateAssets="All" />
Azure DevOps (on-premises Azure DevOps Server):
<PackageReference Include="Microsoft.SourceLink.AzureDevOpsServer.Git" Version="8.0.0" PrivateAssets="All" />
GitLab:
<PackageReference Include="Microsoft.SourceLink.GitLab" Version="8.0.0" PrivateAssets="All" />
Bitbucket:
<PackageReference Include="Microsoft.SourceLink.Bitbucket" Version="8.0.0" PrivateAssets="All" />
All other .csproj properties remain the same. The provider packages differ only in how they compute source file URLs -- each one knows the URL pattern for its hosting provider and generates the mappings accordingly.
If your repository is self-hosted (an on-premises GitHub Enterprise or Azure DevOps Server instance), the packages support additional configuration properties to specify the server hostname. Consult the dotnet/sourcelink repository on GitHub for the per-provider configuration documentation.
One important detail: SourceLink requires that the commit referenced in the PDB is reachable from the remote repository. If you build from a commit that hasn't been pushed yet -- say, a local work-in-progress build -- SourceLink verification will fail because the debugger can't download a file from a commit that doesn't exist on the remote.
Wrapping Up
Adding nuget sourcelink support to your library is a one-time configuration effort that pays dividends across every future release. Your consumers go from staring at disassembly to stepping through actual source. That's a meaningful quality-of-life improvement -- especially when they're hunting down a bug that only reproduces in their integration environment.
The full recipe:
- Add
Microsoft.SourceLink.GitHub(or the appropriate provider package) withPrivateAssets="All" - Set
PublishRepositoryUrl,EmbedUntrackedSources, andDeterministicin your.csproj - Set
<RepositoryUrl>to your repository's remote URL (SourceLink reads this to compute source file URLs) - Choose embedded PDB for simplicity or
.snupkgfor a leaner main package - Push both
.nupkgand.snupkgto NuGet.org - Tell consumers to enable "Source Link support" in their VS debugger settings
For the complete picture of packaging your .NET library from scratch, the complete guide to creating NuGet packages in .NET covers everything from project setup to metadata -- SourceLink fits naturally into that workflow once your package basics are solid.
And while you're building out your packaging pipeline, the article on handling exceptions effectively in C# is a useful companion read -- a library that exposes clear exception types is one that's far easier to debug even without SourceLink.
Once your package is well-structured and debuggable, you'll want to think about how consumers extend it. The article on testing plugin architectures in C# covers patterns for extensible library design that pair well with the debugging improvements you've just enabled.
Step-through debugging is a signal of library quality. Consumers notice when it works -- and they definitely notice when it doesn't.
Frequently Asked Questions
What is nuget sourcelink and why does it matter for library authors?
NuGet SourceLink is a build-time feature that embeds source file URLs from your git repository into the PDB (debug symbols) of your compiled library. When a consumer's debugger loads your NuGet package and requests symbols, it can follow those URLs to download the original source code directly from GitHub (or another provider). The result is step-through debugging into your library's code without requiring consumers to clone your repository. For library authors, SourceLink transforms the debugging experience for every consumer -- which reduces support burden and increases trust in the library.
What is the difference between a nuget symbol package (.snupkg) and an embedded PDB?
A nuget symbol package (.snupkg) is a separate companion package that contains only PDB files. It's pushed to the NuGet.org symbol server and downloaded on demand by debuggers. The main .nupkg stays small. An embedded PDB folds the PDB directly into the compiled DLL, keeping everything in one file -- no symbol server configuration required on the consumer's end. The tradeoff is DLL size: embedded PDBs can significantly increase binary size. For small utility libraries, embedded PDB is simpler. For larger libraries, .snupkg keeps the download lean while still supporting full source-level debugging.
How do I configure pdb nuget support in my .csproj?
Set <DebugType>portable</DebugType>, <IncludeSymbols>true</IncludeSymbols>, and <SymbolPackageFormat>snupkg</SymbolPackageFormat> in your project's <PropertyGroup>. Add a PackageReference for Microsoft.SourceLink.GitHub (version 8.0.0) with PrivateAssets="All". Set <PublishRepositoryUrl>true</PublishRepositoryUrl> and <RepositoryUrl> to your GitHub repository URL. Running dotnet pack with these properties will produce both a .nupkg and a .snupkg in your output directory.
Can consumers use SourceLink without configuring a symbol server?
Yes -- if you choose embedded PDB (<DebugType>embedded</DebugType>), consumers only need to enable "Enable Source Link support" in their Visual Studio debugger options (Tools → Options → Debugging → General). No symbol server URL is required because the PDB is already included in the DLL they installed. If you use .snupkg instead, consumers must additionally add https://symbols.nuget.org/download/symbols to their symbol server list in Visual Studio for symbols to be downloaded.
Does SourceLink work with providers other than GitHub?
Yes. Microsoft provides separate NuGet packages for each major git hosting provider: Microsoft.SourceLink.GitHub, Microsoft.SourceLink.AzureRepos.Git (Azure DevOps cloud / Azure Repos), Microsoft.SourceLink.AzureDevOpsServer.Git (on-premises Azure DevOps Server), Microsoft.SourceLink.GitLab, and Microsoft.SourceLink.Bitbucket. You use the package that matches where your repository is hosted. All other .csproj configuration remains the same -- only the package reference changes. For self-hosted instances (GitHub Enterprise, on-premises Azure DevOps Server), the packages accept additional configuration to specify the server hostname.
How do I verify that SourceLink is correctly configured before publishing?
Install the dotnet-sourcelink global tool with dotnet tool install --global dotnet-sourcelink. Then run dotnet sourcelink test ./path/to/YourLibrary.dll against the built DLL. A passing test confirms every source file in the PDB maps to a valid URL on your remote repository. If it fails, the output identifies which files couldn't be resolved -- usually because the build included uncommitted changes, the repository URL is wrong, or generated files aren't embedded. Run this check in your CI pipeline before pushing a release to catch issues early.
What happens if a consumer tries to step into my library without SourceLink support enabled?
Without SourceLink configured by the library author, or without "Enable Source Link support" checked in Visual Studio, the debugger will either skip over the library call entirely or show decompiled C# code rather than your original source. Decompiled code is often close to the original but can be confusing -- variable names are lost, and the structure may differ from what you wrote. With SourceLink enabled on both sides, consumers see the exact source file from the commit that was used to build the specific version of the package they have installed.

