When developers read API reference, they sometimes have a need or desire to review the corresponding source code. Until recently, the .NET API reference docs did not provide a link back to the source code, prompting calls from the community for this addition. In response to this feedback, we are happy to announce links connecting docs to the source code are now available on most of our popular .NET APIs.
In this blog post, we will share details about how we added the links to the docs experience and how we made use of existing APIs to deliver this improvement.
Live examples of the links
Before going into implementation details, we would like to showcase where the docs have changed. For .NET APIs that meet our required criteria (having Source Link enabled, having accessible PDB, and being hosted in a public repository), the links are included in the Definition metadata
. The following image from the String
class demonstrates the placement of this new link:
In cases where overloads are present, the links are included below the overload title. The following image of String.IndexOf
method demonstrates this pattern:
How do we build the links?
The .NET reference docs pipeline operates on a set of DLL files and NuGet packages. These are processed by a variety of tools to transform their contents into the HTML pages displayed on Microsoft Learn. Correctly building the links to source requires an understanding of the relationship between source, binaries, and GitHub, and how to tie them together with some existing .NET APIs. In discussing our goal to surface links to source with developers from the .NET and Roslyn teams, it became clear that our requirement was closely aligned with Visual Studio’s Go to definition functionality.
With this understanding and the extensive details of Go to definition
provided by @davidwengier in Go To Definition improvements for external source in Roslyn, we were able to apply a similar approach to build links to source for the docs.
Source Link
Source Link is a technology that enables .NET developers to debug the source code of assemblies referenced by their applications. Though originally intended for source debugging, Source Link is perfectly adaptable to our scenario. Every .NET project which enabled Source Link will generate a mapping from a relative folder path to an absolute repository URL in PDB (Program Database). This is as described in the Go To Definition improvements for external source in Roslyn blog post by @davidwengier.
To view the Source Link
entry, you can open the DLL using dotPeek or ILSpy. The following screenshot shows an example accessing the Source Link
entry of System.Private.CoreLib
with dotPeek by navigating to Portable PDB Metadata
then the CustomDebugInformation
table:
[!NOTE] To find out the metadata definition about Source Link, go to: PortablePdb-Metadata.
Building the links
Now we know we have an overall mapping stored in Source Link entry, the next question is how we build a unique link for each type/member in this DLL?
For example, the link we built for String.Clone
method is: https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/String.cs#L388C13-L388C25
This link can be split into 3 parts:
- The first part
https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14
is parsed from Source Link mapping json and is bound to a specific repository commit. - The second part
src/libraries/System.Private.CoreLib/src/System/String.cs
can be found inDocument
table of the PDB. -
And the last part
#L388C13-L388C25
is built fromSequencePoints
column ofMethodDebugInformation
table.SequencePoints
blob will map a range of IL instructions in this method block back to the line numbers of its original source code as demonstrated in below screenshot. For more details, go to SequencePoints Metadata definition.
We use System.Reflection.Metadata library to iterate all the types/members in this DLL and then match the records in MethodDebugInformation
table to build the final links.
var mdReader = peReader.GetMetadataReader();
foreach(var typeDefHandle in mdReader.TypeDefinitions)
{
var typeDef = mdReader.GetTypeDefinition(typeDefHandle);
string typeName = mdReader.GetString(typeDef.Name);
string ns = mdReader.GetString(typeDef.Namespace);
string fullName = String.IsNullOrEmpty(ns) ? typeName : $"{ns}.{typeName}";
Console.WriteLine(fullName);
foreach (var document in debugReader.FindSourceDocuments(typeDefHandle))
{
Console.WriteLine($" {document.SourceLinkUrl}");
}
}
The implementation can also be found in Roslyn DocumentDebugInfoReader.cs and SymbolSourceDocumentFinder.cs.
Finding the PDB file
Since we know the link’s information is available in the PDB, our next step is to locate these PDBs for our use.
Currently given a DLL, we will look for 3 places to locate the corresponding PDB:
- Embedded PDB. If
<DebugType>
embedded</DebugType>
is specified in your csproj, the PDB file will be embedded in this DLL. - PDB on the disk. You can put your PDB right next to your DLL.
- Microsoft Symbol Server. There is a public symbol server where we can download the PDB for the DLL.
See the implementation in Roslyn PdbFileLocatorService.cs.
Finding the correct PDB version
We would like to talk a little more about how we download the correct version of PDB for a given DLL from Microsoft Symbol Server.
Below is a sample PDB download URL and with its format defined in portable-pdb-signature.
http://msdl.microsoft.com/download/symbols/System.Private.CoreLib.pdb/8402667829752b9d0b00ebbc1d5a66d9FFFFFFFF/System.Private.CoreLib.pdb
From the URL pattern we can observe we need to provide the PDB file name System.Private.CoreLib.pdb
and a GUID 8402667829752b9d0b00ebbc1d5a66d9FFFFFFFF
. So the question is where can we find this information?
Previously we used dotPeek to open a DLL to look for the Source Link
entry. Now we can open it again and check the Metadata
section.
In the above screenshot, we can find this GUID in the Debug Directory
and the entry must be a portable code view entry. The Path
attribute of this entry stands for the path to the PDB file which we can get the file name from it.
foreach (var entry in peReader.ReadDebugDirectory())
{
if (entry.Type == DebugDirectoryEntryType.CodeView && entry.IsPortableCodeView)
{
var codeViewEntry = peReader.ReadCodeViewDebugDirectoryData(entry);
var pdbName = Path.GetFileName(codeViewEntry.Path);
var codeViewEntryGuid = $"{codeViewEntry.Guid.ToString("N").ToUpper()}FFFFFFFF";
return $"{MsftSymbolServerUrl}/{pdbName}/{codeViewEntryGuid}/{pdbName}";
}
}
Finding the DLL file
As mentioned earlier, our .NET reference docs pipeline operates on a collection of DLL files or NuGet packages. For some assemblies though we needed to get creative producing the links to source. Here are two situations we needed to develop workarounds for:
- Reference Assembly. For example, DLLs in this package Microsoft.NETCore.App.Ref. Reference assemblies don’t have PDBs uploaded to the symbol server which preventing us from generating the links to source. Our current solution is to download the Runtime package and use the assemblies there to download the matched PDBs.
-
Source embedded in PDB. For example, package System.Threading.AccessControl has source being generated at build time into the
obj
folder.This doesn’t help us link to the source code, so instead of using the DLL in
lib
folder we will also look for DLL with the same name inruntimes
folder.
Consuming the links in the docs pipeline
Once we find the correct DLL/PDB files and successfully build the links to source, we save this information as a JSON file in the target docs GitHub repo.
To understand how we will use this information, we need to revisit the .NET reference docs pipeline. The pipeline creates an XML file for each unique type, which our build system later converts into an HTML page that is presented on Microsoft Learn. To map an API in the XML to its corresponding links to source found in the JSON file we use the unique identifier DocId. This value is present in both the XML (DocId
) and the JSON (DocsId
).
For example, the DocId
for System.String
is T:System.String
. This DocId
value will be used to locate the link to source within the System.Private.CoreLib.json file (for its corresponding version).
"DocsId": "T:System.String",
"SourceLink": "https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/String.cs"
To know about how to generate a DocId
, see DocCommentId.cs or DocumentationCommentId.cs.
Known limitations
In our current implementation we are aware of a few limitations:
- For types with no document info recorded in PDB such as enums or interfaces, a new GUID TypeDefinitionDocuments was introduced in
CustomDebugInformation
table to solve this problem. However this information will be trimmed sometimes for some DLLs and makes us unable to produce the links. See the bug details here https://github.com/dotnet/runtime/issues/100051. - For class members which are defined without a body (e.g. extern or abstract), there is no line information (SequencePoints) included in the PDB. Because of this, we are unable to direct to a span range and instead direct to the entire file. A future improvement is planned to address this.
Another idea for improvement
As you may have noticed, we shared a lot of core logic with Go to definition
. In fact, we reused a couple of their classes in our implementation. A proposed feature we have to improve the process is to modify Roslyn with existing code to generate a source mapping at the type/member level for us to consume.
If the community shares the same requirement, please comment to vote for us. Thanks!
Give us your feedback
We would love to get your feedback on using the links so please let us know what you think! And if you find any issue related to the links, don’t hesitate to share using the feedback controls or open a GitHub issue on the related docs repo.
Lastly, acknowledgments
I want to share thanks to my colleague @shiminxu for his contribution to this project. Also thanks to @ericstj from .NET team and @tmat from Roslyn team for the technical guidance. And finally thanks to the countless others who contributed to make this change possible.
I have used this countless times already since spotting it. It is immensely helpful.
Hello
I follow all commits of dotnet/doc via RSS.
But i cant go github commit page to offical document page. How can i go?
Under link has any live help page link?
https://github.com/dotnet/docs/commit/5213e4d7084b60004013af75a6496f316cf7cbee?short_path=b255437#diff-b255437e97a3327b86484fd453e569e0e124d65e808c143a4db2823c88d74544
It should be this one https://learn.microsoft.com/en-us/dotnet/standard/native-interop/best-practices
You can click search with keyword “native interop best practice” on the right-top corner.
I'm rather interested in the opposite direction: You offer a great F1 experience. However, this only holds for your own libraries. I really liked to see this suggestion about 3d-Party F1 help links being implemented.
I've come to the conclusion that this feature would extremely boost the usability of NuGet libraries, adding to the attractiveness of the whole ecosystem. While the implementation may be more involved, I think it would pay out more.
Your work could become...
Thanks for the suggestion. We will do an evaluation with our PM team for this requirement. Will keep you updated.
Is this functionality only for MS docs or maybe it will be available for everyone in docfx?
Unfortunately it is not available for everyone in docfx. However if Roslyn can directly produce a type/member level source mapping during the build process then I think it would be relatively easy for everyone to integrate with. Please help to vote this feature https://github.com/dotnet/roslyn/issues/71953 thanks.
Thank, it will be really useful.