技术速递|介绍 .NET API 文档的源代码链接

作者: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 功能紧密相关。

凭借这种理解以及 @davidwengierRoslyn 中针对外部源的 Go to definition 改进中提供的有关 Go to definition 的大量细节,我们能够采用类似的方法来构建指向文档源代码的链接。

源代码链接

源代码链接是一种技术,它使 .NET 开发人员能够调试其应用程序引用的程序集的源代码。尽管源代码链接最初旨在用于源代码调试,但它完全适用于我们的场景。每个启用源代码链接的 .NET 项目都会在 PDB(程序数据库)中生成从相对文件夹路径到 绝对存储库 URL 的映射。这与 @davidwengierRoslyn 中针对外部源的 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 个部分:

  1. 第一部分 https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14 是从源代码链接映射 json 解析出来的,并且与特定的存储库提交绑定。

  2. 第二部分 src/libraries/System.Private.CoreLib/src/System/String.cs 可以在 PDB 的文档表中找到。

  3. 最后一部分 #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.csSymbolSourceDocumentFinder.cs 中找到。

查找 PDB 文件

因为我们知道链接的信息可以在 PDB 中找到,所以下一步就是找到这些 PDB 以供我们使用。

目前,指定某一个 DLL,我们会在三个地方查找相应的 PDB:

  1. 嵌入式 PDB。如果您的 csproj 中指定了 embedded,则 PDB 文件将嵌入到此 DLL 中。

  2. 磁盘上的 PDB。您可以将 PDB 放在 DLL 旁边。

  3. 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 包的集合进行操作。但对于某些程序集,我们需要发挥创造力来生成指向源代码的链接。以下是我们需要开发解决方案的两种情况:

  1. 参考程序集。例如, Microsoft.NETCore.App.Ref 包中的 DLL。参考程序集没有将 PDB 上传到符号服务器,这阻止我们生成源代码链接。我们当前的解决方案是下载 Runtime 包并使用其中的程序集下载匹配的 PDB。

  2. 源代码嵌入在 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.csDocumentationCommentId.cs

已知限制

在当前的实施中,我们意识到一些限制:

  1. 对于 PDB 中没有记录文档信息的类型(例如枚举或接口),在 CustomDebugInformation 表中引入了新的 GUID TypeDefinitionDocuments 来解决此问题。但是,对于某些 DLL,这些信息有时会被修剪,导致我们无法生成链接。
    请参阅此处的错误详细信息 https://github.com/dotnet/runtime/issues/100051。

  2. 对于没有定义主体的类成员(例如 extern 或 abstract),PDB 中不包含行信息(SequencePoints)。因此,我们无法指向某个跨度范围,而是指向整个文件。我们计划在未来做出改进以解决此问题。

另一个改进想法

您可能已经注意到,我们与 Go to definition共享了许多核心逻辑。事实上,我们在实现中重用了它们的几个类。我们提出了一个准备用来的改进此过程的功能,即使用现有代码修改 Roslyn,以生成供我们使用的类型/成员级源映射。

如果社区有同样的需求,请评论为我们投票。谢谢!

向我们提供您的反馈

我们很乐意听取您对使用这些链接的反馈,因此请告诉我们您的想法!如果您发现任何与链接相关的问题,请随时使用反馈控件分享或在相关文档存储库上提交 GitHub 问题。
在这里插入图片描述

最后,致谢

我要感谢我的同事@shiminxu 为这个项目做出的贡献。还要感谢 .NET 团队的 @ericstj 和 Roslyn 团队的 @tmat 提供的技术指导。最后,感谢无数为实现这一改变做出的贡献的人。

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值