65.9K
CodeProject 正在变化。 阅读更多。
Home

创建源生成器样板指南 - 第 3 部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2024年1月18日

MIT

10分钟阅读

viewsIcon

3445

一个包含 6 篇文章的系列,为您提供创建源代码生成器的样板指南。

目录

实现源生成器

实现之前

听起来很奇怪,但在创建实际的 Roslyn 源生成器之前,你确实应该先创建生成器。如果你从头开始跟着做,步骤 1 - 规划为当前步骤奠定了基础。

这样做的原因是出于理智。你花在编写、使用或修改 Roslyn 源生成器上的时间越少,整体体验就越好,因为它可能是一项相当令人沮丧的工作。

为了避免实际的生成器,下一步需要使用 T4 模板创建模板。代码生成和 T4 文本模板。这实际上是创建生成对象的旧的、大部分手动的方式。虽然它是旧方式,但它在如何以新方式创建某些东西方面仍然具有价值。通过使用这项技术,你将能够快速充实创建源生成器所需的代码。

创建 T4 模板

创建模板只不过是向你的项目添加一个名为“文本模板”的新项。

以下代码应添加到新生成文件的底部。该代码创建了你应该遵循的基本格式。

<#= Run() #>
<#+
    //
    //	REQUIRED DATA STRUCTURES
    //


    //
    //	RUN METHOD
    //
    public static string Run()
    {
    }


    //
    //	START OF TEMPLATE
    //

#>

基本原理如下

  • <#= Run() #> 这将运行模板并生成输出。它之前或之后的任何文本,包括换行符,也将被包含在内。
  • 部分:数据结构 - 这是你应该放入所需数据结构的地方。这应该是从步骤 2 - 基本设置 BaseClass\Models 进行剪切/粘贴操作。
  • 部分Run 方法 - 这应该是从步骤 1 - 规划中的 Program.Run() 方法进行剪切/粘贴操作。
  • 部分:模板开始 - 这应该是从步骤 1 - 规划中的 FileGenerator.cs 进行剪切/粘贴操作。

Templates\BuilderTemplate.tt 显示了此第一阶段的完整示例。

充实 T4 模板

这是你创建源生成器真正工作的开始。

如果你没有在步骤 1 - 规划中完全充实你的实现,那么现在就是你这样做的时候。一旦你进行了更改并保存了模板文件,就会生成输出。可以通过单击模板文件旁边的箭头来查看输出文件。它将与模板同名,并带有 .txt 扩展名。

警告:不要进行重大更改。如果你进行了更改并且某些功能无法正常工作,那么很难找出你做了什么导致它损坏。

T4 模板仅通过保存模板文件即可实现快速开发。无需构建、运行和调试。我通常将模板和结果文件同时固定在屏幕上,以便我可以编辑、保存并立即看到结果。

在你继续构建最终模板时,继续进行小的更改和保存。添加方法和/或填充已存在的存根。这是一个迭代过程。

如果你在构建时遇到问题,你可能希望返回并修改步骤 1 - 规划中的类。如果它编译并运行正常,你可以将整个类复制回模板文件以供使用。

检查输出的有效性

T4 模板的默认开箱即用格式是文本。你可以将其更改为你期望的格式

  • .cs
  • .xml
  • .json
  • 等等。

通过修改扩展名:<#@ output extension=".txt" #>

当你打开结果输出时,你将获得正在使用的 IDE 的所有好处。它会告诉你所有存在语法错误或可以简化代码的地方。

在你​​的 T4 模板中进行你想要/需要进行的更改,并继续迭代直到它完全正常工作。

警告:完成后将格式改回文本 (.txt),并通过查看文件夹来验证你更改的文件格式是否已不再存在于磁盘上。如果你创建了一个 CSharp 文件,即使它在磁盘上但未显示在你的解决方案中,它也会被编译。

忠告

如果你已完成所有以下步骤和建议,你现在就可以开始创建你的源生成器了。

警告:我再怎么强调遵循所有先前步骤的重要性也不为过。如果你不这样做,那没关系,但请注意,你将面临许多麻烦。

在实际的源生成器中工作并进行更改可能会/将是令人沮丧的。这是无法避免的。这项技术是新的,仍在发展中,而现在,缓存是你最大的敌人。

通过完成所有前期工作,你将在生成器中花费更少的时间,因此在此过程中你会减少很多麻烦。

当事情没有按预期工作时,请进行完整的解决方案清理,关闭 Visual Studio 并重新启动解决方案。在极少数情况下,你可能需要先从磁盘中删除隐藏的 .VS 文件夹,然后再重新打开解决方案。

实现实际的源生成器

  • 将生成输出的 T4 模板中的类复制到一个实际的 CSharp 文件中(参见 GenerateBuilder.cs
  • 创建另一个类来输出你所需的接口(参见 GenerateInterface.cs)。第 16-22 行是可选的,具体取决于你的需求。这将在稍后讨论。
  • 如有必要,更新你的数据结构以匹配 T4 模板中的内容

定义接口

在对象生成过程中,一个属性将被添加到新的测试类中。此属性将用于生成代码。

基本结构是这样的

[GenerateDataBuilder(typeof(Address))]
public partial class AddressBuilder
{
}

生成的属性也将如下所示

namespace WebbertSolutions.Generators;

internal class GenerateDataBuilderAttribute : System.Attribute 
{
    public Type ClassType { get; }


    public GenerateDataBuilderAttribute(System.Type type)
    {
        ClassType = type;
    }
}

这个属性对于每个源生成器都是唯一的。你可能不需要 ClassType 字段。这实际上取决于你的生成器需要做什么。

定义好属性后,你可以将其复制/粘贴到 GenerateInterface.cs 文件中,并将其修改为参数化。

更新生成器类

打开你的类生成器(参见 RandomTestDataGenerator.cs)。

你需要在这里花一些时间实现自定义代码。

  • PostInitializationOutput 中填充对输出接口和在前面部分创建的实际文件生成器的调用。

  • 实现 ScrapeInformation 和/或 ProcessInterface - 这是填充将用于生成输出的数据结构的地方。

    • ScrapeInformation - 从你的接口定义的类中获取信息(例如,Address 类)。
    • ProcessInterface - 从附加了属性的类中获取信息(例如,AddressBuilder 类)。

    我的示例中的这两个类都调用了一个基类来填充数据结构。你可能需要添加、修改或完全替换这些方法的内部以满足你的需求。查看基本实现将为你提供一些关于如何获取所需信息​​的思路。

    如果你无法为上述两个方法找到所需的信息,请将它们存根,以便它们能够编译并继续执行以下步骤。你将不得不等到步骤 4 - 调试时再填写。

此时,你应该尝试构建你的解决方案并修复所有必要的语法错误和引用。

测试源生成器

Hic sunt dracones... (此地有恶龙...)
通往疯狂的道路从这里开始...

从此刻起所做的更改可能需要关闭 Visual Studio 并重新打开项目才能看到更改或使生成器工作。这将是令人作呕的,所以习惯它吧。

到目前为止的所有前期工作都是为了最大限度地减少这个耗时/令人沮丧的步骤。

  • 创建一个新的测试库(MyTestAppTests

    • 添加对源生成器的项目引用 - 这在实际实现中将通过添加 NuGet 包来管理,但我们还没有到那一步。

    • 打开项目并将以下内容添加到新添加的源生成器项目引用中

      OutputItemType="Analyzer" ReferenceOutputAssembly="false"

      这确保了生成器 DLL 不会作为最终产品的一部分输出,并且应该被识别为分析器。

  • 重新构建整个解决方案,并确保没有错误。

  • 在你的新库中创建一个测试类,并放入你的生成器运行所需的​​内容(参见 PersonBuilder.cs

    • 将属性注释掉
    • 同时添加属性的命名空间。我将我的放在 _GlobalUsings.cs 文件中。
  • 再次构建,然后关闭 Visual Studio。

  • 重新加载解决方案。

  • 取消注释命名空间和属性。

到此为止,祈祷一切都能正常工作。这​​是基于你遵循了所有前面的部分和步骤的事实。

查看输出

有两种方法可以查看输出。

  • 查看测试项目中的分析器
    • MyTestAppTests -> Dependencies -> Analyzers -> RandomTestDataGenerator
  • 发出输出
    • 打开测试项目并在 PropertyGroup 部分添加以下内容

      <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
      警告:不要长期将此设置为 true。这将阻止生成器自动运行。为了查看输出,需要进行构建。
    • MyTestAppTests -> obj -> Debug -> <.net version> -> generated -> RandomTestDataGenerator

查找性能问题

RandomTestDataGenerator 中包含 Execute() 方法。其中包含一个字典,用于计算生成器为每个正在生成的对象运行的次数。这意味着 AddressBuilderPersonBuilderStateBuilder 都应该随着时间的推移具有不同的值。每次在 Visual Studio 中打开解决方案时,计数器都会重置。

生成器包含一个参数来输出值。这不是必需的,仅用于确定源生成器是否在每次更改时都在频繁运行。

唯一应该导致生成器运行的更改是以下情况:

  • 添加/删除/重命名属性/字段
  • 更改属性/字段的数据类型
  • 更改正在检查的对象的命名空间

  • 在同一文件中添加注释或空格

这完全取决于你实际在 Models 文件夹的类中捕获和存储的内容。

通过进行更改,包括注释,并查看日期/时间戳何时更改或计数器何时增加,你将能够确定你的比较器是否完整且工作正常。

故障排除

这里真的没有什么神奇的公式可以帮助你。

本教程的设置使得你可以使用 WinMerge 等工具来比较所提供的教程文件夹结构,并查看步骤之间进行了哪些有意义的更改。希望你能发现你遗漏的内容并能够纠正问题。

如果你的结构缺少数据,或者在生成过程中出现问题,例如 null 引用异常,你将需要设置生成器以便可以对其进行调试。

调试将在下一节中介绍。

历史

  • 2024年1月18日:初始版本
© . All rights reserved.