📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What are Just My Code, symbols (PDB files) and Source Link, and how do you step into framework or NuGet package code?

How the debugger finds source for code you did not write: portable PDBs, symbol servers and .snupkg packages, Just My Code, Source Link and embedded sources, with settings for Visual Studio, VS Code and Rider.

Junior Theory Intermediate debugging just-my-code pdb source-link symbols

🎯 Key points

  • A PDB maps compiled IL to source lines and local variable names; the .NET SDK produces portable PDBs by default
  • Just My Code makes the debugger step only through your code, which keeps everyday stepping and exception breaking fast and focused
  • To step into framework or NuGet code: turn off Just My Code, load symbols from the Microsoft and NuGet.org symbol servers, and enable Source Link
  • Source Link records the repository URL and commit in the PDB so the debugger downloads the exact source
  • Library authors publish symbols as .snupkg packages and enable Source Link and deterministic CI builds
  • Deploying PDBs gives production exception logs file and line numbers and makes dumps readable
  • Optimised library code may still show some variables as unavailable
  • Only use trusted symbol servers; downloaded source is code from someone else's repository

⏱ 30-second answer

A PDB file maps compiled IL back to source lines and variable names; without it the debugger can't show source or bind breakpoints. Just My Code tells the debugger to step only through my code and treat everything else as external. To step into ASP.NET Core or a NuGet package, I turn Just My Code off, enable the Microsoft and NuGet.org symbol servers so the debugger can download PDBs, and enable Source Link. Source Link stores the repository URL and commit in the PDB, so the debugger fetches the exact source file that was compiled. Library authors make this work by publishing .snupkg symbol packages from deterministic CI builds.

🗣 Full answer (senior)

How I would answer this in an interview

"A PDB file maps compiled IL back to source lines and local variable names, and the .NET SDK produces portable PDBs by default. Without a matching PDB the debugger can't show source or bind breakpoints, and the match is strict: a PDB from a different build won't load, which is a common reason for 'symbols not loaded'. Just My Code tells the debugger to step only through my own code and collapse everything else into [External Code] on the Call Stack, which keeps everyday stepping and exception breaking focused.

To step into ASP.NET Core or a NuGet package, I turn Just My Code off, enable the Microsoft and NuGet.org symbol servers so the debugger downloads the PDBs, and make sure Source Link is on. Source Link records the repository URL and commit in the PDB, so the debugger fetches the exact source that was compiled, without cloning anything. Framework code is optimised, so some variables may still show as unavailable.

For libraries we publish, I ship a .snupkg symbol package with Source Link and deterministic CI builds using ContinuousIntegrationBuild. For applications, I deploy the PDBs so production exception logs have file and line numbers and memory dumps stay readable. The mistake I see is leaving Just My Code off permanently, so I turn it off only for the session where I need to read library code, and I only use trusted symbol servers, because downloaded source is someone else's code. Debuggability is a feature of what you ship."


Symbols

When the C# compiler builds an assembly it also writes a PDB (program database). The .NET SDK produces portable PDBs by default, which work on every OS. The PDB contains:

  • sequence points mapping IL offsets to file, line and column;
  • local variable names and scopes;
  • optionally Source Link JSON and even embedded source files.

The debugger matches a PDB to its DLL by a unique id, so a PDB from a different build will not load. That is why "symbols not loaded" is a common reason a breakpoint will not bind.

Just My Code

With Just My Code on (the default in Visual Studio, "justMyCode": true in VS Code), the debugger treats code as yours when it is non-optimised and has symbols loaded. Stepping skips other code, the Call Stack collapses it into [External Code], and exceptions are reported where they leave framework code and enter yours.

Stepping into framework and package code

Visual Studio VS Code (C# extension) Rider
Just My Code Tools → Options → Debugging → General → Enable Just My Code (untick) "justMyCode": false in launch.json Debugger settings → Enable external source debug
Symbols Debugging → Symbols → tick Microsoft Symbol Servers and NuGet.org Symbol Server "symbolOptions" with Microsoft and NuGet.org servers Uses symbol servers, and can decompile assemblies without symbols
Source Enable Source Link support (on by default) Source Link supported Source Link or decompiled source

The first time, symbol downloads are slow; they are cached locally afterwards. To make optimised framework code easier to inspect, Visual Studio can also Suppress JIT optimisation on module load, at a performance cost.

Source Link for your own libraries

Source Link writes the repository URL and commit hash into the PDB. The .NET 8 and later SDKs include Source Link support for common hosts (GitHub, Azure Repos, GitLab, Bitbucket), so for a library you mostly set publishing properties: produce a .snupkg symbol package, set ContinuousIntegrationBuild in CI for deterministic paths, and push both packages to the feed.

Production example

A team suspected a bug in an internal NuGet package that handled ABN validation. Because the package published .snupkg symbols to the internal Azure Artifacts feed with Source Link, a developer turned off Just My Code, stepped straight into the package at the exact commit, and found a missing check-digit case in minutes, without cloning the other repository or guessing at the version.

🔍 What the interviewer is checking

The interviewer is checking whether you can debug beyond your own code and understand how the tooling finds source.

Green flags

  • Explains what a PDB contains and why mismatches break breakpoints.
  • Knows Just My Code, symbol servers and Source Link, and how to turn each on.
  • Mentions .snupkg and deterministic builds for library authors.

Red flags

  • Thinks you need to clone a library to debug it.
  • Removes PDBs to "protect" code.
  • Never heard of Source Link.

📚 Learn in depth

1. From DLL to source line

 Orders.Api.dll is loaded by the runtime
      |
      v
 debugger reads the DLL's debug directory: PDB id (GUID + stamp) and file name
      |
      v
 search for a PDB with the SAME id, in order:
   1. embedded inside the DLL (<DebugType>embedded</DebugType>)
   2. next to the DLL
   3. local symbol cache
   4. symbol servers: Microsoft, NuGet.org, your Azure Artifacts feed
      |
      +-- not found or id mismatch --> "symbols not loaded": no source, no breakpoints
      |
      v
 PDB gives: sequence points + local names + Source Link JSON
      |   {"documents": {"/_/*": "https://raw.githubusercontent.com/org/repo/<commit>/*"}}
      v
 debugger maps /_/src/Abn/AbnValidator.cs to that URL at that exact commit
      |
      v
 downloads the file (once, then cached) --> shows source, binds breakpoints

The id match is strict. A PDB from a different build of the same code won't load, which protects you from seeing source that doesn't match the running code.

2. What a portable PDB contains

Content Used for
Sequence points Map IL offsets to file, line and column for breakpoints and stepping
Local variable and constant names, scopes Locals and Watch windows
Document table with checksums Verify the source file matches what was compiled
Source Link JSON Where to download each document
Embedded source (optional) Show source without any download
Async and iterator method info Stepping through state machines

The .NET SDK produces portable PDBs by default on every OS. The older Windows-only PDB format is only needed for a few legacy tools.

3. Just My Code: what counts as "my code"

Visual Studio treats a module as user code when:

  • its symbols are loaded, and
  • it isn't optimised (a Debug build), and
  • it isn't marked as non-user code (for example with [DebuggerNonUserCode]).
With Just My Code on With it off
Step Into skips framework and package code Step Into enters any code with symbols
Call Stack collapses other code into [External Code] Every frame is shown
Exceptions break where they leave framework code and enter yours Exceptions break where thrown, even inside libraries
Breakpoints in library code may not bind Breakpoints bind if symbols load

The default is on because most debugging is about your own code.

4. Step-by-step: stepping into ASP.NET Core or a NuGet package

Step Visual Studio VS Code (C# extension) Rider
1. Turn off Just My Code Tools > Options > Debugging > General > untick Enable Just My Code "justMyCode": false in launch.json Debugger settings > enable external source debugging
2. Enable symbol servers Debugging > Symbols > tick Microsoft Symbol Servers and NuGet.org Symbol Server "symbolOptions" with both servers Uses symbol servers automatically
3. Source Enable Source Link support (on by default) Source Link supported Source Link, or decompiled source
4. Optional: make framework code easier to inspect Suppress JIT optimization on module load "suppressJITOptimizations": true Similar option in debugger settings
5. Restart debugging

The first session is slow while symbols download; they are cached afterwards. In Visual Studio, "Load only specified modules" avoids downloading symbols for everything.

When no symbols exist at all, Visual Studio and Rider can decompile the assembly so you can still read and step through it, with generated names instead of the original locals.

5. Source Link for your own libraries

Since the .NET 8 SDK, Source Link support for GitHub, Azure Repos, GitLab and Bitbucket is built in, so a library mostly needs publishing properties:

Property Effect
IncludeSymbols + SymbolPackageFormat=snupkg Produces a .snupkg symbol package alongside the .nupkg
PublishRepositoryUrl Records the repository URL in the package and PDB
EmbedUntrackedSources Embeds files not in git, such as source generator output
ContinuousIntegrationBuild (on CI only) Deterministic, path-mapped build: local paths become /_/
DebugType=embedded (optional) PDB inside the DLL; no separate symbol package needed

Pushing the .nupkg to NuGet.org or Azure Artifacts pushes the .snupkg next to it, and the feed serves it as a symbol server.

6. Deterministic builds and path mapping

Without ContinuousIntegrationBuild, a PDB contains paths such as D:\a\1\s\src\Abn\AbnValidator.cs from the build agent. With it, the compiler maps the repository root to /_/, so:

  • builds of the same commit produce identical output (reproducible builds);
  • agent folder structures don't leak into published packages;
  • Source Link has a stable prefix to map to the repository URL.

Set it only on CI. Locally you want real paths so the IDE opens your own files.

7. Symbols in production

  • Deploy PDBs with the app (or use embedded PDBs) so exception stack traces logged in production include file and line numbers.
  • When analysing a dump from production, the dotnet-symbol tool downloads matching symbols for the modules in the dump, for example dotnet-symbol -o ./symbols ./core_20261022. Then point dotnet-dump or Visual Studio at that folder.
  • Keep PDBs for every release you ship, for example in your artifact store or an internal symbol server, so old dumps stay readable.

8. Security

  • Symbol servers and Source Link fetch content from the network. Configure only servers you trust. For private repositories the IDE uses your signed-in account to download source.
  • Downloaded source is someone else's code. Reading it is fine; don't copy it into your project without checking the licence.
  • PDBs reveal method names, local names and source paths, but they don't add secrets that aren't already in the code. Shipping PDBs is usually acceptable and makes crash dumps far more useful. Never put secrets in source in the first place.

9. .NET 11 awareness

Portable PDBs, Source Link and .snupkg are stable. Watch the .NET 11 SDK release notes for changes to default packaging or Source Link providers.

10. Try it yourself

  1. In an ASP.NET Core app, set a breakpoint in an endpoint and look at the Call Stack: [External Code].
  2. Turn off Just My Code, enable the Microsoft and NuGet.org symbol servers, and restart debugging.
  3. Show external code in the Call Stack, double-click a Kestrel or routing frame, and watch Source Link download the file.
  4. Open Debug > Windows > Modules and check the symbol status and symbol file path for Microsoft.AspNetCore.Routing.dll.
  5. Pack one of your own libraries with the code example's .csproj and open the .snupkg as a zip to see the PDB.
  6. Build the library twice with ContinuousIntegrationBuild=true and compare the DLL hashes: they match.

💻 Code

Library .csproj set up for symbols and Source Link:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <PackageId>Contoso.Abn</PackageId>
    <Version>1.4.0</Version>

    <!-- Symbols published as a separate .snupkg -->
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>

    <!-- Source Link: record the repo URL and embed files not in source control -->
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
    <EmbedUntrackedSources>true</EmbedUntrackedSources>

    <!-- Deterministic, path-mapped builds on CI (GitHub Actions sets GITHUB_ACTIONS=true) -->
    <ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
  </PropertyGroup>
</Project>

Pack and push both packages:

dotnet pack -c Release -o ./artifacts
dotnet nuget push "./artifacts/*.nupkg" --source "https://api.nuget.org/v3/index.json" --api-key "$NUGET_API_KEY"

VS Code launch.json settings to step into external code:

{
  "name": "Api (external code)",
  "type": "coreclr",
  "request": "launch",
  "program": "${workspaceFolder}/src/Api/bin/Debug/net10.0/Api.dll",
  "justMyCode": false,
  "suppressJITOptimizations": true,
  "symbolOptions": {
    "searchMicrosoftSymbolServer": true,
    "searchNuGetOrgSymbolServer": true
  }
}

⚠ Common mistakes

  • Mistake: turning off Just My Code permanently. Everyday stepping wanders into framework code, the Call Stack fills with external frames, and exceptions break inside libraries that handle them. Instead: turn it off only for the session where you need to read library code, then turn it back on.

  • Mistake: expecting optimised framework code to be fully inspectable. Framework assemblies are Release builds, so some locals show as unavailable and stepping can jump. Instead: enable Suppress JIT optimization on module load (or suppressJITOptimizations in VS Code) while you investigate, and accept it changes performance.

  • Mistake: mixing a DLL from one build with a PDB from another. The ids don't match, so the debugger refuses the PDB and breakpoints stay hollow. Instead: keep each DLL and its PDB together from the same build, or embed the PDB in the DLL.

  • Mistake: configuring only one IDE. Visual Studio keeps these settings under Tools > Options > Debugging; VS Code needs justMyCode and symbolOptions in launch.json. Changing one doesn't change the other. Instead: set them in each tool your team uses, and commit a shared launch.json configuration for external code debugging.

  • Mistake: loading symbols for every module on a slow connection. Every session waits on hundreds of downloads. Instead: use "Load only specified modules", or load symbols on demand from the Modules window.

  • Mistake: publishing a library without symbols or Source Link. Consumers can't step into it, so they clone the wrong version or guess. Instead: publish a .snupkg, set PublishRepositoryUrl and EmbedUntrackedSources, and build with ContinuousIntegrationBuild on CI.

  • Mistake: setting ContinuousIntegrationBuild for local builds. Paths in the PDB become /_/..., so the IDE can't open your local files when you debug. Instead: set it conditionally, only when a CI environment variable is present.

  • Mistake: adding untrusted symbol servers. The debugger downloads and opens whatever they serve. Instead: use the Microsoft and NuGet.org servers and your organisation's own feed only.

  • Mistake: deleting PDBs from deployments "to protect the code". Production stack traces lose line numbers and dumps become much harder to read, while the DLL can be decompiled anyway. Instead: ship PDBs with the app or keep them in a symbol store for every release, and keep secrets out of source.

🏛 Senior insight: trade-offs

Seniors treat debuggability as a feature of what they ship. For internal libraries they make symbols, Source Link and deterministic builds part of the package template, so anyone can step into the exact version they use instead of guessing. For applications they keep PDBs with the deployment (or in a symbol server) so stack traces have line numbers and dumps are readable. And they turn Just My Code off only for the session where they need to read library code, because the default is optimised for the common case.

🔗 Follow-ups the interviewer may ask

  1. When: The candidate mentions [External Code] frames. Why: Just My Code directly changes what the Call Stack shows.
    How to answer

    The Call Stack shows every method that is currently active on the thread, newest at the top: who called whom to reach this line. I double-click a lower frame to jump to that caller, and Locals and Watch switch to that frame's variables, which is how I find where a bad value came from. Just My Code hides framework frames as [External Code]. For async code the real thread stack after an await starts in the thread pool, so Visual Studio stitches the logical async chain back together, and Parallel Stacks shows it across tasks. In Release builds, inlining can hide frames.

  2. When: The candidate says to keep PDBs with deployments. Why: Symbols are what make production dumps readable.
    How to answer

    When I can't attach a debugger, I take a dump: a snapshot of the process's memory, threads and stacks that I analyse offline. For a hang or a leak, dotnet-dump collect -p <pid> does it in seconds. For crashes, I enable automatic crash dumps with the DOTNET_DbgEnableMiniDump environment variables. On App Service I use Diagnose and solve problems, and in Kubernetes dotnet-monitor. Then I analyse with dotnet-dump analyze: clrstack -all and syncblk for hangs, dumpheap -stat and gcroot for leaks, pe for exceptions. Or I open the dump in Visual Studio and use the Threads and Parallel Stacks windows. Dumps contain customer data, so they're handled as sensitive.

↩ Usually asked after

More in Debugging Tools

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me