Skip to content

Portable PDB, SourceLink & Symbol Servers

Key Points

  • Portable PDB — the cross-platform .NET symbol format, replacing legacy Windows PDBs. Smaller, faster, and the default since .NET Core.
  • Embedded PDBs — pack symbols into the assembly itself (<DebugType>embedded</DebugType>). Trade-off: larger DLL, simpler distribution.
  • SourceLink — embeds source-control URLs into PDBs so debuggers can step into NuGet package source code without local clones.
  • Symbol servers — host PDBs separately (NuGet.org, internal Azure Artifacts) so consumers download symbols on demand.
  • Senior setup: deterministic build + embedded SourceLink + published symbols package (.snupkg) for libraries; embedded for apps; CI publishes both.

Concepts (deep dive)

PDB formats

Format Platform Default since
Windows PDB Windows-only, MSF format Pre-.NET Core
Portable PDB Cross-platform, ECMA-335 metadata format .NET Core 1.0
Embedded PDB Inside the DLL/EXE .NET Core 2.0
<PropertyGroup>
  <DebugType>embedded</DebugType>           <!-- inside the DLL -->
  <!-- or -->
  <DebugType>portable</DebugType>            <!-- separate .pdb file -->
</PropertyGroup>

Why PDBs matter

  • Stack traces — without PDBs, traces show only Method+0x47; with PDBs, file/line numbers.
  • Debugger — set breakpoints in source.
  • Profilers — sample reports map to functions.

Without symbols, a production crash dump is much harder to diagnose.

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

<ItemGroup>
  <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>

Effect: build embeds a srcsrv map in the PDB pointing each source file to its raw URL on GitHub (or other host). Visual Studio / Rider / VS Code can fetch source on demand and step into NuGet package code.

Source File: Order.cs
  https://raw.githubusercontent.com/myorg/myrepo/abc123def/src/Order.cs

Deterministic builds

<PropertyGroup>
  <Deterministic>true</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

Produces byte-identical assemblies across machines, given the same inputs. Required for reproducible builds and SourceLink path normalization. CI pipelines should set ContinuousIntegrationBuild=true to strip local paths from PDBs.

Symbol packages (.snupkg)

dotnet pack --include-symbols --include-source -p:SymbolPackageFormat=snupkg

Produces MyLib.1.0.0.nupkg + MyLib.1.0.0.snupkg. Push both:

dotnet nuget push MyLib.1.0.0.nupkg --api-key $KEY --source nuget.org
dotnet nuget push MyLib.1.0.0.snupkg --api-key $KEY --source nuget.org

Consumers' debuggers fetch symbols from https://symbols.nuget.org/download/symbols.

Symbol servers

  • NuGet.org symbol server — public, hosts .snupkg-published symbols.
  • Microsoft public symbol serverhttps://msdl.microsoft.com/download/symbols for OS / runtime symbols.
  • Internal Azure Artifacts / NuGet feeds — host private package symbols.

Configure VS / Rider:

Tools → Options → Debugging → Symbols → add server URL

Or nuget.config:

<configuration>
  <packageSources>
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
  <symbolServers>
    <add key="nuget.org symbols" value="https://symbols.nuget.org/download/symbols" />
  </symbolServers>
</configuration>

dotnet symbol tool

dotnet tool install -g dotnet-symbol
dotnet symbol --recurse-subdirectories ~/dumps

Downloads matching PDBs (and runtime symbols, if requested) for a directory of binaries. Used in dump-investigation workflows.

Embedded vs separate PDB trade-off

Embedded:
  + Single artifact, no PDB to ship
  - Larger DLL (~20-50% bigger)
  - Symbols always present

Separate:
  + Smaller DLL
  - Two files; need to deploy/index PDBs

For OSS libraries — embedded is friendlier. For products — separate, ship PDBs to symbol server.

Compiler-emitted info

Modern PDBs include: - File paths and line numbers per IL offset. - Local variable names (debug builds; release strips most). - Source file content (<EmbedAllSources>true</EmbedAllSources>). - Source URL map (SourceLink). - Async state-machine info for friendlier async stack traces.

Crash dumps + symbols

dotnet-dump analyze core.dump
> setsymbolpath SRV*c:\symbols*https://msdl.microsoft.com/download/symbols
> clrstack

Without symbols, frames are unreadable. Always ship/publish symbols for production binaries.


Code: correct vs wrong

❌ Wrong: ship release binary without symbols

<PropertyGroup>
  <DebugType>none</DebugType>
</PropertyGroup>

Production crashes are unsearchable.

<PropertyGroup>
  <DebugType>embedded</DebugType>
  <Deterministic>true</Deterministic>
  <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
  <PublishRepositoryUrl>true</PublishRepositoryUrl>
</PropertyGroup>
<ItemGroup>
  <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>

❌ Wrong: NuGet package without symbols

dotnet pack    # only the .nupkg; consumers can't step in

✅ Correct: include .snupkg

dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
dotnet nuget push *.snupkg --source nuget.org

Design patterns for this topic

Pattern 1 — Library SDK template

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Deterministic>true</Deterministic>
    <ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
    <DebugType>embedded</DebugType>
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
    <EmbedUntrackedSources>true</EmbedUntrackedSources>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
  </ItemGroup>
</Project>

Reusable as a Directory.Build.props snippet.

Pattern 2 — Internal symbol server

Host .snupkg to a private feed (Azure Artifacts, MyGet, JFrog). CI publishes symbols on every release; engineers configure their IDEs to use the internal feed.

Pattern 3 — Production dump → symbol fetch pipeline

  1. Capture dump (dotnet-dump collect) on incident.
  2. Upload to incident store.
  3. CI job runs dotnet symbol to fetch matching PDBs from internal/public servers.
  4. Engineer opens dump in dotnet-dump analyze or VS with full call stacks.

Pattern 4 — EmbedAllSources for self-contained debugging

<EmbedAllSources>true</EmbedAllSources>

Pack the source code of every file referenced by the PDB into the PDB. Larger but completely portable — no source-control fetch needed.


Pros & cons / trade-offs

Choice Pros Cons
Embedded PDB Single artifact, foolproof Larger DLL
Separate PDB Smaller DLL Two files to ship
SourceLink only Tiny PDB, source on demand Requires source-control access
EmbedAllSources Fully self-contained Largest size
.snupkg to NuGet Public step-in support Two pushes, separate auth

When to use / when to avoid

  • Apps: embedded + deterministic + SourceLink. One artifact, debuggable.
  • Libraries: .snupkg published alongside, deterministic, SourceLink to public repo.
  • Closed-source products: separate PDBs to internal symbol server only.
  • Avoid <DebugType>none</DebugType> in production builds. Always have something.

Interview Q&A

Q1. What's a portable PDB? A. Cross-platform .NET symbol format using ECMA-335 metadata, replacing the Windows-only MSF PDB format. Smaller, faster, and works identically on Linux/macOS/Windows.

Q2. Why use embedded PDBs? A. Single deployment artifact — no separate .pdb file to lose. Trade-off: larger DLL. Common for OSS libraries.

Q3. What does SourceLink do? A. Embeds a map in the PDB linking source-file paths to a public URL (e.g., GitHub raw). Debuggers fetch source on demand, letting you step into NuGet packages without local clones.

Q4. What's a .snupkg? A. NuGet's symbol package format — separate from the main .nupkg. Hosts PDBs and SourceLink data. Pushed alongside the regular package.

Q5. Why deterministic builds matter? A. Same inputs produce byte-identical outputs across machines and times. Enables reproducible builds, supply-chain verification, and SourceLink (which embeds normalized paths).

Q6. How does the NuGet symbol server work? A. Consumers configure their debugger to query https://symbols.nuget.org/download/symbols; the server returns matching PDBs by GUID/Age (PDB ID). Without it, debuggers can't find symbols for NuGet packages.

Q7. What's EmbedAllSources? A. Build option that copies every source file referenced by the PDB into the PDB itself. Makes the PDB fully self-contained — no source-control fetch needed for debugging — at the cost of size.

Q8. Why does CI fail with Source path is not under root on PDB? A. PDBs embed source paths; on different machines those paths differ. Set ContinuousIntegrationBuild=true to normalize paths via SourceLink, restoring determinism.


Gotchas / common mistakes

  • Pushing .nupkg to NuGet but not the .snupkg — consumers can't step in.
  • Forgetting ContinuousIntegrationBuild=true in CI — paths leak local user directories.
  • <DebugType>none</DebugType> in production — unstable for crash diagnosis.
  • Symbols built from a different commit than the binary — lookup fails (PDB GUID mismatch).
  • Not including SourceLink package — PDBs lack source URLs.
  • Embedded PDBs that bloat AOT-trimmed apps — measure size; consider portable for AOT.
  • Symbol server timeout in restricted networks — configure offline cache.

Further reading