作者:Min Huang,Matt Trilby-Bassett
排版:Alan Wang
开发人员在阅读 API 参考文档时,有时会需要或希望查看相应的源代码。直到不久之前,.NET API 参考文档还没有提供指向源代码的链接,这引起社区添加这一功能的呼声。针对这一反馈,我们很高兴地宣布,现在大多数流行的 .NET API 上都提供了连接文档和源代码的链接。
在这篇博文中,我们将分享将链接添加到文档以及利用现有 API 来实现这一改进的详细信息。
链接的实例
在介绍实施细节之前,我们想展示一下文档的改动。对于符合我们标准(启用了源代码链接、具有可访问的 PDB 并托管在公共存储库中)的 .NET API,其链接包含在 Definition 元数据中。以下来自 String 类的截图演示了这个新链接的位置:
如果存在重载,链接将包含在重载标题的下面。下面的 String.IndexOf 方法截图演示了这种情况:
我们如何建立链接?
.NET 参考文档管道对一组 DLL 文件和 NuGet 包进行操作。这些文件由各种工具处理,以将其内容转换为显示在 Microsoft Learn 上的 HTML 页面。正确构建源代码的链接需要了解源代码、二进制文件和 GitHub 之间的关系,以及它们如何与一些现有的 .NET API 配合在一起。在与 .NET 和 Roslyn 团队的开发人员讨论我们公开源代码链接的目标时,很明显我们的要求与 Visual Studio 的 Go to definition 功能紧密相关。
凭借这种理解以及 @davidwengier 在 Roslyn 中针对外部源的 Go to definition 改进中提供的有关 Go to definition 的大量细节,我们能够采用类似的方法来构建指向文档源代码的链接。
源代码链接
源代码链接是一种技术,它使 .NET 开发人员能够调试其应用程序引用的程序集的源代码。尽管源代码链接最初旨在用于源代码调试,但它完全适用于我们的场景。每个启用源代码链接的 .NET 项目都会在 PDB(程序数据库)中生成从相对文件夹路径到 绝对存储库 URL 的映射。这与 @davidwengier 在 Roslyn 中针对外部源的 Go to definition 改进中所述一致。
若要查看源链接条目,可以使用 dotPeek 或 ILSpy 打开 DLL。以下屏幕截图显示了使用 dotPeek 访问 System.Private.CoreLib 的源链接条目的示例,方法是导航到 Portable PDB Metadata,然后导航到 CustomDebugInformation 表:
[!NOTE] 若要了解有关源代码链接的元数据定义,请转到:PortablePdb-Metadata。
建立链接
现在我们知道在源代码链接条目中存储了一个总体映射,下一个问题是如何为这个 DLL 中的每个类型/成员构建唯一的链接?
例如,我们为 String.Clone 方法构建的链接是:
https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/String.cs#L388C13-L388C25
此链接可分为 3 个部分:
-
第一部分 https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14 是从源代码链接映射 json 解析出来的,并且与特定的存储库提交绑定。
-
第二部分 src/libraries/System.Private.CoreLib/src/System/String.cs 可以在 PDB 的文档表中找到。
-
最后一部分 #L388C13-L388C25 是基于 MethodDebugInformation 表的 SequencePoints 列构建的。SequencePoints blob 会将此方法块中的一系列 IL 指令映射回其原始源代码行号,如下面的屏幕截图所示。有关更多详细信息,请转到 SequencePoints 元数据定义。
我们使用 System.Reflection.Metadata 库来遍历此 DLL 中的所有类型/成员,然后匹配 MethodDebugInformation 表中的记录以构建最终的链接。
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}");}
}
该实现也可以在 Roslyn DocumentDebugInfoReader.cs 和 SymbolSourceDocumentFinder.cs 中找到。
查找 PDB 文件
因为我们知道链接的信息可以在 PDB 中找到,所以下一步就是找到这些 PDB 以供我们使用。
目前,指定某一个 DLL,我们会在三个地方查找相应的 PDB:
-
嵌入式 PDB。如果您的 csproj 中指定了 embedded,则 PDB 文件将嵌入到此 DLL 中。
-
磁盘上的 PDB。您可以将 PDB 放在 DLL 旁边。
-
Microsoft Symbol Server。有一个公共符号服务器,我们可以从中下载 DLL 的 PDB。
请参阅 Roslyn PdbFileLocatorService.cs 中的实现。
查找正确的 PDB 版本
我们想进一步讨论如何从 Microsoft Symbol Server 下载指定 DLL 的正确版本的 PDB。
下面是一个PDB 下载 URL 的示例 ,其格式在 portable-pdb-signature 中定义。
http://msdl.microsoft.com/download/symbols/System.Private.CoreLib.pdb/8402667829752b9d0b00ebbc1d5a66d9FFFFFFFF/System.Private.CoreLib.pdb
从 URL 模式中我们可以观察到,我们需要提供 PDB 文件名 System.Private.CoreLib.pdb 和 GUID 8402667829752b9d0b00ebbc1d5a66d9FFFFFFFF。那么问题是我们可以在哪里找到这些信息?
之前我们使用 dotPeek 打开 DLL 来查找源代码链接条目。现在我们可以再次打开它并检查元数据部分。
在上面的截图中,我们可以在 Debug Directory 中找到这个 GUID,并且该条目必须是一个可移植代码视图条目。该条目的 Path 属性代表 PDB 文件的路径,我们可以从中获取文件名。
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}";}
}
查找 DLL 文件
如前所述,我们的 .NET 参考文档管道对 DLL 文件或 NuGet 包的集合进行操作。但对于某些程序集,我们需要发挥创造力来生成指向源代码的链接。以下是我们需要开发解决方案的两种情况:
-
参考程序集。例如, Microsoft.NETCore.App.Ref 包中的 DLL。参考程序集没有将 PDB 上传到符号服务器,这阻止我们生成源代码链接。我们当前的解决方案是下载 Runtime 包并使用其中的程序集下载匹配的 PDB。
-
源代码嵌入在 PDB 中。例如,System.Threading.AccessControl 包在构建时会将源代码生成到 obj 文件夹中。
使用文档管道中的链接
一旦我们找到正确的 DLL/PDB 文件并成功建立源代码的链接,我们就会将此信息以 JSON 文件形式保存在目标文档 GitHub 存储库中。
为了了解我们将如何使用这些信息,我们需要重新审视 .NET 参考文档管道。管道为每种唯一类型创建一个 XML 文件,我们的构建系统稍后会将其转换为显示在 Microsoft Learn 上的 HTML 页面。为了将 XML 中的 API 映射到 JSON 文件中找到的相应源代码链接,我们使用唯一标识符 DocId。此值存在于 XML(DocId)和 JSON(DocsId)中。
例如,System.String 的 DocId 为 T:System.String。此 DocId 值将用于定位 System.Private.CoreLib.json 文件(其对应版本)中的源代码链接。
"DocsId": "T:System.String",
"SourceLink": "https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/String.cs"
若要了解如何生成 DocId,请参阅 DocCommentId.cs 或 DocumentationCommentId.cs。
已知限制
在当前的实施中,我们意识到一些限制:
-
对于 PDB 中没有记录文档信息的类型(例如枚举或接口),在 CustomDebugInformation 表中引入了新的 GUID TypeDefinitionDocuments 来解决此问题。但是,对于某些 DLL,这些信息有时会被修剪,导致我们无法生成链接。
请参阅此处的错误详细信息 https://github.com/dotnet/runtime/issues/100051。 -
对于没有定义主体的类成员(例如 extern 或 abstract),PDB 中不包含行信息(SequencePoints)。因此,我们无法指向某个跨度范围,而是指向整个文件。我们计划在未来做出改进以解决此问题。
另一个改进想法
您可能已经注意到,我们与 Go to definition共享了许多核心逻辑。事实上,我们在实现中重用了它们的几个类。我们提出了一个准备用来的改进此过程的功能,即使用现有代码修改 Roslyn,以生成供我们使用的类型/成员级源映射。
如果社区有同样的需求,请评论为我们投票。谢谢!
向我们提供您的反馈
我们很乐意听取您对使用这些链接的反馈,因此请告诉我们您的想法!如果您发现任何与链接相关的问题,请随时使用反馈控件分享或在相关文档存储库上提交 GitHub 问题。
最后,致谢
我要感谢我的同事@shiminxu 为这个项目做出的贡献。还要感谢 .NET 团队的 @ericstj 和 Roslyn 团队的 @tmat 提供的技术指导。最后,感谢无数为实现这一改变做出的贡献的人。