源生成器
本文概述了 .NET Compiler Platform(“Roslyn”)SDK 附带的源生成器。 通过源生成器,C# 开发人员可以在编译用户代码时检查用户代码。 生成器可以动态创建新的 C# 源文件,这些文件将添加到用户的编译中。 这样,代码可以在编译期间运行。 它会检查你的程序以生成与其余代码一起编译的其他源文件。
源生成器是 C# 开发人员可以编写的一种新组件,允许执行两个主要操作:
检索表示正在编译的所有用户代码的编译对象。 可以检查此对象,并且可以编写适用于正在编译的代码的语法和语义模型的代码,就像现在使用分析器一样。
生成可在编译过程中添加到编译对象的 C# 源文件。 也就是说,在编译代码时,可以提供其他源代码作为编译的输入。
结合使用这两项操作能充分发挥源生成器的强大功能。 可以使用编译器在编译时构建的丰富元数据检查用户代码。 然后,生成器将 C# 代码发送回基于已分析数据的同一编译。 如果你熟悉 Roslyn 分析器,可以将源生成器视为可发出 C# 源代码的分析器。
源生成器作为编译阶段运行,如下所示:
源生成器是由编译器与任何分析器一起加载的 .NET Standard 2.0 程序集。 它在可以加载和运行 .NET Standard 组件的环境中使用。
重要
目前 .NET Standard 2.0 程序集只能用作源生成器。
常见方案
以下三种常规方法可用于检查用户代码,并基于当今技术所使用的分析生成信息或代码:
- 运行时反射。
- 处理 MSBuild 任务。
- 交织中间语言 (IL)(本文未进行讨论)。
源生成器可以是对以上每种方法的改进。
运行时反射
运行时反射是很久以前就添加到 .NET 中的一项强大技术。 使用该技术的场景不计其数。 一种常见的场景是在应用启动时对用户代码进行一定分析,并使用这些数据生成内容。
例如,ASP.NET Core 在 Web 服务首次运行时使用反射来发现你已定义的构造,使其能够“连接”控制器和 razor 页等内容。 虽然这使你能够使用强大的抽象编写简单的代码,但会在运行时影响性能:当 Web 服务或应用首次启动时,它无法接受任何请求,直到所有发现你的代码相关信息的运行时反射代码都运行完毕后才可以。 虽然这种性能影响不显著,但这是一个固定的成本,你无法在自己的应用中自我改进。
借助源生成器,启动的控制器发现阶段可以发生在编译时。 生成器可以分析源代码并发出“连接”应用所需的代码。 使用源生成器可能会加快启动时间,因为如今在运行时发生的操作可能会被推送到编译时。
处理 MSBuild 任务
源生成器也可以通过其他方式改进性能,从而发现类型,并不局限于运行时的反射。 有些场景需要多次调用 MSBuild C# 任务(称为 CSC),以便它们可以检查编译中的数据。 可以想象到的是,多次调用编译器会影响生成应用所需的总时间。 我们正在研究如何使用源生成器来避免像这样同时处理多项 MSBuild 任务,因为源生成器不仅提供了一定的性能优势,还允许工具在正确的抽象级别上运行。
源生成器可以提供的另一项功能是避免使用某些“强类型”的 API,例如 ASP.NET Core 在控制器和 razor 页面之间的路由方式。 使用源生成器时,路由可以为强类型,所需的字符串作为编译时细节生成。 这可以减少键入错误字符串文本导致请求未命中正确控制器的次数。
源生成器入门
在本指南中,你将了解如何使用 ISourceGenerator API 创建源生成器。
创建 .NET 控制台应用程序。 此示例使用 .NET 6。
将
Program
类替换为以下代码。 以下代码不使用顶级语句。 经典格式是必需的,因为第一个源生成器在该Program
类中编写分部方法:namespace ConsoleApp; partial class Program { static void Main(string[] args) { HelloFrom("Generated Code"); } static partial void HelloFrom(string name); }
备注
你可以按原样运行此示例,但不会执行任何操作。
接下来,我们将创建一个源生成器项目来实现
partial void HelloFrom
方法对应项。创建一个以
netstandard2.0
目标框架名字对象 (TFM) 为目标的 .NET 标准库项目。 添加 NuGet 包 Microsoft.CodeAnalysis.Analyzers 和 Microsoft.CodeAnalysis.CSharp:<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" /> </ItemGroup> </Project>
提示
源生成器项目需要以
netstandard2.0
TFM 为目标,否则它将不起作用。创建一个名为 HelloSourceGenerator.cs 的新 C# 文件,该文件指定你自己的源生成器,如下所示:
using Microsoft.CodeAnalysis; namespace SourceGenerator { [Generator] public class HelloSourceGenerator : ISourceGenerator { public void Execute(GeneratorExecutionContext context) { // Code generation goes here } public void Initialize(GeneratorInitializationContext context) { // No initialization required for this one } } }
源生成器需要同时实现 Microsoft.CodeAnalysis.ISourceGenerator 接口,并且具有 Microsoft.CodeAnalysis.GeneratorAttribute。 并非所有源生成器都需要初始化,本示例实现就是这种情况,其中 ISourceGenerator.Initialize 为空。
将 ISourceGenerator.Execute 方法的内容替换为以下实现:
using Microsoft.CodeAnalysis; namespace SourceGenerator { [Generator] public class HelloSourceGenerator : ISourceGenerator { public void Execute(GeneratorExecutionContext context) { // Find the main method var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken); // Build up the source code string source = $@"// <auto-generated/> using System; namespace {mainMethod.ContainingNamespace.ToDisplayString()} {{ public static partial class {mainMethod.ContainingType.Name} {{ static partial void HelloFrom(string name) => Console.WriteLine($""Generator says: Hi from '{{name}}'""); }} }} "; var typeName = mainMethod.ContainingType.Name; // Add the source code to the compilation context.AddSource($"{typeName}.g.cs", source); } public void Initialize(GeneratorInitializationContext context) { // No initialization required for this one } } }
从
context
对象中,我们可以访问编译的入口点或Main
方法。mainMethod
实例是一个 IMethodSymbol,它表示一个方法或类似方法的符号(包括构造函数、析构函数、运算符或属性/事件访问器)。 Microsoft.CodeAnalysis.Compilation.GetEntryPoint 方法返回程序的入口点的 IMethodSymbol。 其他方法使你可以查找项目中的任何方法符号。 在此对象中,我们可以推理包含的命名空间(如果存在)和类型。 此示例中的source
是一个内插字符串,它对要生成的源代码进行模板化,其中内插的缺口填充了包含的命名空间和类型信息。 使用提示名称将source
添加到context
。 对于此示例,生成器创建一个新的生成的源文件,其中包含控制台应用程序中partial
方法的实现。 可以编写源生成器来添加任何喜欢的源。提示
GeneratorExecutionContext.AddSource 方法中的
hintName
参数可以是任何唯一名称。 通常为该名称提供显式 C# 文件扩展名,例如".g.cs"
或".generated.cs"
。 该文件名有助于将文件标识为正在生成源。现在,我们有一个正常运行的生成器,但需要将其连接到控制台应用程序。 编辑原始的控制台应用程序项目,并添加以下内容,将项目路径替换为你在上面创建的 .NET Standard 项目中的路径:
<!-- Add this as a new ItemGroup, replacing paths and names appropriately --> <ItemGroup> <ProjectReference Include="..\PathTo\SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> </ItemGroup>
新引用不是传统的项目引用,必须手动编辑以包含
OutputItemType
和ReferenceOutputAssembly
属性。 有关ProjectReference
的OutputItemType
和ReferenceOutputAssembly
属性的更多信息,请参阅常见的 MSBuild 项目项:ProjectReference。现在,运行控制台应用程序时,应会看到生成的代码运行并打印到屏幕。 控制台应用程序本身不实现
HelloFrom
方法,而是在编译过程中从源生成器项目生成的源。 以下文本是来自此应用程序的示例输出:Generator says: Hi from 'Generated Code'
备注
由于正在积极改进工具体验,因此可能需要重启 Visual Studio 才能看到 IntelliSense 并消除错误。
如果使用的是 Visual Studio,则可以看到源生成的文件。 在“解决方案资源管理器”窗口中,展开“依赖项”>“分析器”>“SourceGenerator”>“SourceGenerator.HelloSourceGenerator”,然后双击“Program.g.cs”文件。
打开这个生成的文件时,Visual Studio 将指示该文件是自动生成的并且无法编辑。
还可以设置生成属性以保存生成的文件并控制生成的文件的存储位置。 在控制台应用程序的项目文件中,将
<EmitCompilerGeneratedFiles>
元素添加到<PropertyGroup>
,将其值设置为true
。 再次生成项目。 现在,生成的文件是在 obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator 下创建的。 路径的组成部分映射到生成器的生成配置、目标框架、源生成器项目名称和完全限定的类型名称。 可以通过将<CompilerGeneratedFilesOutputPath>
元素添加到应用程序的项目文件来选择较方便的输出文件夹。
后续步骤
源生成器指南介绍了其中一些示例,并提供了一些建议的解决方法。 此外,我们在 GitHub 上提供了一组示例,你可以自己尝试。
若要详细了解源生成器,请参阅下列文章: