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





5.00/5 (1投票)
一个包含 6 篇文章的系列,为您提供创建源代码生成器的样板指南。
目录
- 第 1 部分 - 介绍 / 准备创建源代码生成器
- 第 2 部分 - 创建源代码生成器的基本基础设施
- 第 3 部分 - 实现源代码生成器
- 第 4 部分 - 调试源代码生成器
- 第 5 部分 - 将源代码生成器打包成 NuGet 包
- 第 6 部分 - 如何使用 NuGet 源代码生成器
实现源生成器
实现之前
听起来很奇怪,但在创建实际的 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 模板中进行你想要/需要进行的更改,并继续迭代直到它完全正常工作。
忠告
如果你已完成所有以下步骤和建议,你现在就可以开始创建你的源生成器了。
在实际的源生成器中工作并进行更改可能会/将是令人沮丧的。这是无法避免的。这项技术是新的,仍在发展中,而现在,缓存是你最大的敌人。
通过完成所有前期工作,你将在生成器中花费更少的时间,因此在此过程中你会减少很多麻烦。
当事情没有按预期工作时,请进行完整的解决方案清理,关闭 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()
方法。其中包含一个字典,用于计算生成器为每个正在生成的对象运行的次数。这意味着 AddressBuilder
、PersonBuilder
和 StateBuilder
都应该随着时间的推移具有不同的值。每次在 Visual Studio 中打开解决方案时,计数器都会重置。
生成器包含一个参数来输出值。这不是必需的,仅用于确定源生成器是否在每次更改时都在频繁运行。
唯一应该导致生成器运行的更改是以下情况:
- 添加/删除/重命名属性/字段
- 更改属性/字段的数据类型
- 更改正在检查的对象的命名空间
非
- 在同一文件中添加注释或空格
这完全取决于你实际在 Models 文件夹的类中捕获和存储的内容。
通过进行更改,包括注释,并查看日期/时间戳何时更改或计数器何时增加,你将能够确定你的比较器是否完整且工作正常。
故障排除
这里真的没有什么神奇的公式可以帮助你。
本教程的设置使得你可以使用 WinMerge 等工具来比较所提供的教程文件夹结构,并查看步骤之间进行了哪些有意义的更改。希望你能发现你遗漏的内容并能够纠正问题。
如果你的结构缺少数据,或者在生成过程中出现问题,例如 null
引用异常,你将需要设置生成器以便可以对其进行调试。
调试将在下一节中介绍。
历史
- 2024年1月18日:初始版本