在 C# 中生成代码的四种方式——包括.NET 5中的Source Generators
在 .NET 中,我们有以下几种方法来帮助我们生成代码:
- Code snippets.
- Reflection.
- T4 Template.
- [New] Source Generators in .NET 5.
应该还有更多,但本文将主要覆盖这四种方式。您可以参考我发布在GitHub上的demo: https://github.com/yanxiaodi/MyCodeSamples/tree/main/CodeGeneratorDemo. 让我们开始吧!
Code snippets
Code snippets 是可重用的代码块,可以使用热键组合将其插入我们的代码文件中。例如,如果在Visual Studio中键入 prop
然后按 Tab
,VS将在您的类中自动生成一个属性,然后您可以轻松地替换属性名称。VS已经为我们提供了大量的内置的代码片段,如 prop
, if
, while
, for
, try
,您可以在这里找到所有的默认代码片段列表: C# Code Snippets
[1]
。
Code snippets 的好处是您可以替换参数。例如,当我们将MVVM模式用于UWP / Xamarin / WPF应用程序时,经常需要在实现 INotifyPropertyChanged
[2]
接口的类中创建属性。如果您使用 MvvmCross
框架,它可能看起来像这样:
private ObservableCollection _commentList;
public ObservableCollection CommentList
{
get => _commentList;
set => SetProperty(ref _commentList, value);
}
我们不想复制/粘贴然后更改变量名,所以我创建了一个 Code snippet 来简化工作。创建一个名为 myMvvm.snippet
的新文件,然后复制并粘贴以下代码:
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
<Title>MvvmCross property</Title>
<Author>Xiaodi Yan</Author>
<Shortcut>mvxprop</Shortcut>
<Description>
A property in a ViewModel in the Xamarin project with MvvmCross.
</Description>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>Property</ID>
<ToolTip>Property name</ToolTip>
<Default>Property</Default>
</Literal>
<Object>
<ID>type</ID>
<ToolTip>Property type</ToolTip>
<Default>string</Default>
</Object>
<Literal>
<ID>pProperty</ID>
<ToolTip>Private property name</ToolTip>
<Default>property</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[#region $Property$;
private $type$ _$pProperty$;
public $type$ $Property$
{
get => _$pProperty$;
set => SetProperty(ref _$pProperty$, value);
}
#endregion]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
在此 Code snippet 中,我们使用
指定快捷方式 mvxprop
,并使用
声明一些参数。例如,我们声明了一个名为的参数 Property
,然后使用 $Property
将其插入到代码段中。您可以通过VS Tools
菜单中的 Code Snippets Manager
导入此 Code snippet(或按 Ctrl + K,Ctrl + B
)。
现在,您可以键入 mvxprop
并按 Tab
,VS可以为您创建属性-您只需手动替换属性名称即可。
更多信息请参考:
-
Walkthrough: Create a code snippet
[3] -
Code snippet functions
[4] -
How to: Distribute code snippets
[5]
Code snippets 适合重复使用以插入整个类或方法或属性。您还可以将 Code snippets 分发给其他用户。当我们创建新文件或 Class 或 Method 时,这很有用。但是,如果要在完成后更新生成的代码,则必须删除现有代码,然后重新创建它。基本上,它可以节省无聊的复制/粘贴时间,但仅此而已。
Reflection
Reflection(反射)广泛用于许多.NET框架和库中,例如 ASP.NET Core
[6]
, Entity Framework Core
[7]
等。它可以提供 类型的
[8]
对象,该对象描述程序集,模块和类型,以便您可以动态创建类型的实例,从现有对象获取类型,然后调用其方法或访问其字段和属性。
当我们构建.NET应用程序时,它将生成程序集-例如.dll文件。这些程序集包含我们的模块,其中包含某些类型。类型包含成员。Reflection 能够获取这些信息。因此,我们可以动态加载新的.dll文件并调用它们的方法或事件,而无需编辑代码。 动态
表示它可以在运行时运行。换句话说,当我们编译应用程序时,.NET应用程序直到运行时才知道我们需要使用什么类型。通过这种方式,我们可以创建一个客户端,该客户端可以根据我们的规则动态执行其他程序集中的方法。如果我们遵循该规则更新其他程序集中的类,则不需要更新客户端代码。
让我们查看以下示例。您可以在我的示例项目中找到它。我们在 CodeGeneratorDemo.ReflectionDemo.Core
项目中有一个 ISpeaker
接口,如下所示:
namespace CodeGeneratorDemo.ReflectionDemo.Core
{
public interface ISpeaker
{
string SayHello();
}
}
创建两个实现类:
ChineseSpeaker
:
namespace CodeGeneratorDemo.ReflectionDemo.Core
{
public class ChineseSpeaker : ISpeaker
{
public string Name => this.GetType().ToString();
public string SayHello()
{
return "Nihao";
}
}
}
以及 EnglishSpeaker
:
namespace CodeGeneratorDemo.ReflectionDemo.Core
{
public class EnglishSpeaker : ISpeaker
{
public string Name => this.GetType().ToString();
public string SayHello()
{
return "Hello!";
}
}
}
现在,我们可以使用 Reflection 来查找 ISpeaker
接口的所有实现,并调用其方法或属性。
在 CodeGeneratorDemo.ReflectionDemo
项目中创建一个名为 ReflectionHelper
的新文件:
using CodeGeneratorDemo.ReflectionDemo.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace CodeGeneratorDemo.ReflectionDemo
{
public class ReflectionHelper
{
public static List GetAvailableSpeakers()
{
// You can also use AppDomain.CurrentDomain.GetAssemblies() to load all assemblies in the current domain.
// Get the specified assembly.
var assembly =
Assembly.LoadFrom(Path.Combine(Directory.GetCurrentDirectory(), "CodeGeneratorDemo.ReflectionDemo.Core.dll"));
// Find all the types in the assembly.
var types = assembly.GetTypes();
// Apply the filter to find the implementations of ISayHello interface.
var result = types.Where(x => x.IsClass && typeof(ISpeaker).IsAssignableFrom(x)).ToList();
// Or you can use types.Where(x => x.IsClass && x.GetInterfaces().Contains(typeof(ISpeaker))).ToList();
return result;
}
}
}
在此类中,我们加载包含所需类型的指定dll文件。然后,我们可以使用 Reflection 并应用LINQ查询来找到所有 ISpeaker
接口的实现。
在 CodeGeneratorDemo.Client
项目中,我们可以输出每个Speaker的Name属性和调用 SayHello
方法:
private static void ReflectionSample()
{
Console.WriteLine("Here is the Reflection sample:");
// Find all the speakers in the current domain
var availableSpeakers = ReflectionHelper.GetAvailableSpeakers();
foreach (var availableSpeaker in availableSpeakers)
{
// Create the instance of the type
var speaker = Activator.CreateInstance(availableSpeaker);
// Get the property info of the given property name
PropertyInfo namePropertyInfo = availableSpeaker.GetProperty("Name");
// Then you can get the value of the property
var name = namePropertyInfo?.GetValue(speaker)?.ToString();
Console.WriteLine($"I am {name}");
// Invoke the method of the instance
Console.WriteLine(availableSpeaker.InvokeMember("SayHello", BindingFlags.InvokeMethod, null, speaker, null));
}
Console.WriteLine();
}
运行该程序,您将看到以下输出:
Here is the Reflection sample:
I am CodeGeneratorDemo.ReflectionDemo.Core.ChineseSpeaker
Nihao
I am CodeGeneratorDemo.ReflectionDemo.Core.EnglishSpeaker
Hello!
如果我们需要添加其他语言的其他Speaker,只需在同一项目中添加实现类。.NET Reflection 可以自动找出所有必需的类并正确调用方法。
当我们创建插件类型的应用程序时,它非常有用。首先,我们创建接口并通过反射从客户端调用方法。然后,我们可以在客户端界面之后创建插件,这些插件可以作为* .dll文件动态加载并执行。
另一种情况是框架开发。作为框架开发人员,您将无法知道用户将创建哪些实现,因此只能使用 Reflection 来创建这些实例。例如在某些MVVM框架中,如果按照约定创建类,如xxxViewModel,该框架可以找到所有 ViewModel 并使用 Reflection 自动加载它们。
通常,当人们谈论反射时,主要关注的是性能。因为它在运行时运行,所以从理论上讲,它比普通应用程序要慢一点。但是它在许多情况下都非常灵活,尤其是在开发框架的情况下。如果可以接受程序花费几秒钟(或仅几百毫秒)来加载程序集,则使用Reflection是没有问题的。
使用Reflection的所需的主要名称空间是 System.Reflection
[9]
和 System.Type
[10]
。您可能还需要了解以下术语:
-
Assembly
[11] -
Module
[12] -
ConstructorInfo
[13] -
MethodInfo
[14] -
FieldInfo
[15] -
EventInfo
[16] -
PropertyInfo
[17] -
ParameterInfo
[18] -
CustomAttributeData
[19]
更多信息请参考以下文档:
-
Reflection in .NET
[20] -
Viewing Type Information
[21] -
Dynamically Loading and Using Types
[22]
T4 Template
T4 Text Template是文本块和可以生成文本文件的控制逻辑的混合体。 T4
表示 text template transformation
。您可以使用它在Visual Studio 中为 C# 和 Visual Basic 生成文件。但是生成的文件本身可以是任何类型的文本,例如* .txt文件,HTML文件或任何语言的程序源代码。您可以使用C#代码(或VB)来控制模板中的逻辑。几年前,我曾经使用NuGet包(EntityFramework Reverse POCO Generator)为EntityFramework生成POCO模型。它由T4 Template 实现。我只需要更新T4 Template 中的数据库连接字符串并保存它,然后T4 Template 就可以读取数据库信息并自动创建所有模型和方法。
T4 Template 有两种: 运行时
和 设计时
。区别在于,运行时T4 Template在应用程序中执行以生成文本字符串。它将创建一个包含 TransformText()
方法的 .cs类。即使目标计算机未安装Visual Studio,也可以调用此方法来生成字符串。与此不同的是,对设计时T4 Template来说,当您在Visual Studio中保存模板时,会生成原始源代码或文本文件。如果要使用运行时T4 Template,则需要将文件的
Custom Tool 属性设置为
TextTemplatingFilePreprocessor
。对于设计时T4 Template, Custom Tool
属性应设置为 TextTemplatingFileGenerator
。
您可以在 CodeGeneratorDemo.T4TemplateDemo
项目中找到示例,包含两个T4 Template: RunTimeTextTemplateDemo.tt
和 DesignTimeTextTemplateDemo.tt
。
运行时 T4 Template
要正确生成项目,您需要安装 System.CodeDom
NuGet软件包。打开 RunTimeTextTemplateDemo.tt
文件,对HTML代码进行一些更改,然后将其保存。您将看到T4 Template 自动更新生成的文件 RunTimeTextTemplateDemo.cs
。其中包含一个可以在客户端代码中调用的 TransformText()
方法。
<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
Sales for Previous Month
<# for (int i = 1; i <= 10; i++)
{ #>
Test name <#= i #>
Test value <#= i * i #>
<# } #>
This report is Company Confidential.
每次保存模板时,它将更新生成的文件。在客户端代码中,我们可以这样调用:
var page = new RunTimeTextTemplateDemo();
Console.WriteLine(page.TransformText());
您将在控制台中看到生成的HTML代码。
设计时 T4 Template
设计时模板只能在开发程序时在Visual Studio中使用。它会生成原始文本文件-可以是.cs,.html或.txt或其他任意格式的文本文件。通常,您将需要定义一个 model
,可以是文本文件(XML或JSON或csv或其他)或数据库,然后模板从模型中读取数据并生成一些源代码。
这是一个例子:
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ output extension=".cs" #>
using System;
using System.Threading.Tasks;
namespace CodeGeneratorDemo.T4TemplateDemo.DesignTimeTextTemplateDemo
{
<#
var models = new List<string>();
// You can read the data from any source you have.
string path = Path.Combine(Path.GetDirectoryName(this.Host.TemplateFile), "dataSource.txt");
if(File.Exists(path))
{
models = File.ReadAllText(path).Split(',').ToList();
}
foreach (var model in models)
{
#>
public partial class <#=model#>
{
public Guid Id { get; set; }
public <#=model#>(Guid id)
{
Id = id;
}
}
public partial class <#=model#>Service
{
public Task<<#=model#>> Get(Guid id)
{
return Task.FromResult(new <#=model#>(id));
}
}
<#
}
#>
}
保存模板时,T4 Template 可以为每个类生成模型和服务。
如何创建T4 Template
从上面的示例中可以看到,T4 Template由以下部分组成:
-
指令-控制模板处理方式的元素。
-
文本块-直接复制到输出的原始文本。
-
控制块-将变量值插入文本中并控制文本的有条件或重复部分的程序代码。
例如,您可以使用以下指令指定输出文件格式:
<#@ output extension=".txt" #>
您也可以使用C#代码控制逻辑。例如,检查以下代码:
<#
for(int i = 0; i < 4; i++)
{
#>
Hello!
<#
}
#>
它将输出 Hello
四次。在此示例中, Hello
是一个文本块,而该 for
语句只是C#代码。
要使用变量,可以使用表达式控制块。只需使用
输出变量,如下所示:
<#
string message = "Hello";
for(int i = 0; i < 4; i++)
{
#>
<#=message#>
<#
}
#>
它将输出 Hello
四次。
T4模板的强大功能是,您可以导入程序集并使用所需的大多数.NET库,例如:
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
请注意,您需要将它们放置在原始文本和控制块之前。您甚至可以在控制块中使用反射。有了这些功能,我们可以为某些情况编写非常有用的模板。
调试 T4 Template
像普通的C#程序一样,我们可以通过设置断点来调试T4 Template。要调试设计时T4 Template,请右键单击该模板,然后从Solution Explorer中的文件菜单中选择 Debug T4 template
。要调试运行时T4 Template,只需调试项目,因为它会在程序编译时运行。
T4 Template 编辑器
默认情况下,Visual Studio不支持语法着色和智能感知等。幸运的是,我们有一些VS扩展来提高工作效率,例如 DevArt T4 Editor
[23]
。您可以在VS扩展市场中搜索 T4 Template
,您将找到更多。
我们不会在本文中介绍T4模板的所有详细信息。有关更多信息,请阅读以下文档:
-
Code Generation and T4 Text Templates
[24] -
Walkthrough: Generate Code by using Text Templates
[25] -
Run-Time Text Generation with T4 Text Templates
[26] -
T4 Text Template Directives
[27] -
Text Template Control Blocks
[28] -
Guidelines for Writing T4 Text Templates
[29]
Source Generators in .NET 5
要开始使用Source Generators,您需要安装最新的 .NET 5 SDK
[30]
。
什么是 Source Generator?它是如何工作的?
根据微软的定义:
A Source Generator is a piece of code that runs during compilation and can inspect your program to produce additional files that are compiled together with the rest of your code.
让我们回顾一下Reflection的工作原理。如前所述,在构建应用程序时,Reflection代码直到应用程序运行时才知道它将使用什么类型。这就是为什么人们抱怨Reflection的性能。如果在应用启动时要加载很多程序集,则可能会对性能产生轻微的影响。这个问题很难解决,因为这是Reflection的弊端-您可以从开发中受益,但是您必须接受它的缺点。
Source Generators可用于解决性能问题-至少,提高性能是其重要目标之一。Source Generators可以分析当前源代码,并在代码编译过程中生成一些将与当前源代码一起编译的代码-换句话说,当应用程序完成编译时,它已经完全知道它将使用哪种类型。这是改进的关键。
这是Microsoft提供的的Source Generators的示意图:
我们需要知道的一件事是, 源生成器只能向代码中添加内容,而不能更改任何现有代码
。让我们来看一个例子。
第一个 Source Generator 实例
Source Generate 需要实现 Microsoft.CodeAnalysis.ISourceGenerator
接口:
namespace Microsoft.CodeAnalysis
{
public interface ISourceGenerator
{
void Initialize(GeneratorInitializationContext context);
void Execute(GeneratorExecutionContext context);
}
}
创建一个名为 CodeGeneratorDemo.SourceGeneratorDemo
的新.NET Standard 2.0 Class项目。安装以下两个NuGet软件包:
- Microsoft.CodeAnalysis.CSharp v3.8+
- Microsoft.CodeAnalysis.Analyzers v3.3+
我们还需要将语言版本指定为 preview
:
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
从技术上讲,源生成器还不是C#的正式功能,现在仍在预览中。因此,我们需要明确指定preview版本。
然后在项目中创建一个 SpeakersSourceGenerator.cs
文件。更新内容,如下所示:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Text;
namespace CodeGeneratorDemo.SourceGeneratorDemo
{
[Generator]
public class SpeakersSourceGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// Not needed for this sample
}
public void Execute(GeneratorExecutionContext context)
{
// begin creating the source we'll inject into the users compilation
var sourceBuilder = new StringBuilder(@"
using System;
namespace CodeGeneratorDemo.SourceGeneratorDemo
{
public static class SpeakerHelper
{
public static void SayHello()
{
Console.WriteLine(""Hello from generated code!"");
");
sourceBuilder.Append(@"
}
}
}");
// inject the created source into the users compilation
context.AddSource("speakersSourceGenerator", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
}
}
该 SpeakersSourceGenerator
类实现了 ISourceGenerator
接口,并具有 Generator
属性。程序编译时,它将找到Source Generators并生成我们需要的代码。在此示例中,我仅创建了一个名为 SpeakerHelper
的类,包含一个 SayHello()
方法。如果我们正确生成了代码,它将在控制台中输出消息。
接下来,将引用添加到 CodeGeneratorDemo.Client
项目。请注意,您需要像这样更新项目文件:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CodeGeneratorDemo.SourceGeneratorDemo\CodeGeneratorDemo.SourceGeneratorDemo.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"/>
</ItemGroup>
</Project>
您还需要指定语言版本。另外,由于我们没有将项目引用为普通的dll文件,因此我们需要更新 OutputItemType
和 ReferenceOutputAssembly
的值,如上所示。
在客户端代码中添加代码:
private static void SourceGeneratorSample()
{
CodeGeneratorDemo.SourceGeneratorDemo.SpeakerHelper.SayHello();
}
您可能会看到VS报错,找不到 CodeGeneratorDemo.SourceGeneratorDemo.SpeakerHelper
,因为我们的代码中还没有这个类。Source Generators的工具仍在预览中,因此我们需要构建 CodeGeneratorDemo.SourceGeneratorDemo
项目并关闭VS,然后重新启动它。然后,您会发现VS可以支持智能感知了。当我们构建它时,Source Generators实际上会生成 SpeakerHelper
类。现在运行客户端应用程序,我们可以看到输出,来自生成的代码:
Hello from generated code!
因此,这个过程是,当我们构建项目时,将调用Source Generators来生成一些可以与原始源代码一起编译的代码。这样,就不会出现性能问题,因为它发生在编译中。当应用程序启动时,生成的代码已与其他源代码一起编译。
根据我的经验,有时VS无法识别生成的方法或类,只要构建正确运行即可。
如果在客户端代码中按一下 F12
以检查 SayHello()
方法,您将看到生成的文件,该文件显示此文件无法编辑:
您可能很好奇文件在哪里。如果要查看实际生成的文件,可以将以下部分添加到 CodeGeneratorDemo.SourceGeneratorDemo
项目和 CodeGeneratorDemo.Client
项目中:
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
然后,您可以在 obj/GeneratedFiles
文件夹中找到该文件。如果未指定 CompilerGeneratedFilesOutputPath
属性,则该属性应位于 obj/SourceGeneratorFiles
文件夹中。
这只是一个非常简单的示例,展示了如何在运行时之前生成代码。接下来,让我们看另一个更复杂的示例。
在编译时生成Attribute
考虑以下场景:当我们使用依赖注入时,通常我们需要手动注册实例。对于此演示,我将创建一个 Attribute
[31]
来装饰需要注册的类。我们可以使用Reflection来检索这些属性以找到特定的类,但是操作可能很昂贵。使用Source Generators,我们可以在编译时生成代码,以在运行时之前对其进行注册。
创建一个新类 AutoRegisterSourceGenerator
,如下所示:
[Generator]
public class AutoRegisterSourceGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// TODO
}
public void Execute(GeneratorExecutionContext context)
{
// TODO
}
}
接下来,让我们创建Attribute。我们可以创建一个实际的类,但是为了进行演示,我将使用Source Generator生成它。将以下代码添加到 AutoRegisterSourceGenerator
:
private const string AttributeText = @"
using System;
namespace CodeGeneratorDemo.SourceGeneratorDemo
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
sealed class AutoRegisterAttribute : Attribute
{
public AutoRegisterAttribute()
{
}
}
}";
这只是一个字符串。接下来,更新 Execute
方法以将字符串添加到源代码中:
public void Execute(GeneratorExecutionContext context)
{
context.AddSource("AutoRegisterAttribute", SourceText.From(AttributeText, Encoding.UTF8));
}
当我们构建项目时,它将生成 AutoRegisterAttribute
。
下一步是创建一些接口:
namespace CodeGeneratorDemo.Client.Core
{
public interface IOrderService
{
}
public interface IProductService
{
}
}
还有一些实现类,例如 OrderService
和 ProductService
,由 AutoRegister
属性装饰:
using System;
using CodeGeneratorDemo.SourceGeneratorDemo;
namespace CodeGeneratorDemo.Client.Core
{
[AutoRegister]
public class OrderService : IOrderService
{
public OrderService()
{
Console.WriteLine($"{this.GetType()} constructed.");
}
}
[AutoRegister]
public class ProductService : IProductService
{
public ProductService()
{
Console.WriteLine($"{this.GetType()} constructed.");
}
}
}
目前,我们的代码中没有 AutoRegister
。因此,您将看到VS报错。没关系,因为稍后Source Generator会生成它。
我们将调用另一个类 DiContainerMocker
来模拟DI容器:
using System;
namespace CodeGeneratorDemo.Client.Core
{
public static class DiContainerMocker
{
public static void RegisterService(TImplementation service)
{
Console.WriteLine($"{service.GetType()} has been registered for {typeof(TInterface)}.");
}
}
}
Source Generators
依赖于 Roslyn
[32]
。它可以检查要编译的数据。我们可以使用称为 SyntaxReceivers
的对象来访问 SyntaxTrees
,然后根据这些信息进行迭代其中的 SyntaxNodes
,然后生成代码。
创建一个名为 MySyntaxReceiver
的新类,该类实现了 ISyntaxReceiver
接口:
public class MySyntaxReceiver : ISyntaxReceiver
{
public List CandidateClasses { get; } = new List();
///
/// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
///
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// any method with at least one attribute is a candidate for property generation
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax
&& classDeclarationSyntax.AttributeLists.Count >= 0)
{
CandidateClasses.Add(classDeclarationSyntax);
}
}
}
在这个类中,我们将检查每个 SyntaxNode
。如果它是一个Class并且具有Attribute,那么我们将其添加到列表中。
接下来,我们需要在Source Generator的 Initialize
方法中注册 MySyntaxReceiver
:
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
}
现在来完成我们的Source Generator。我的想法是,我们将依次检查每个 SyntaxNode
,如果它是一个Class并具有 AutoRegister
属性那么就生成注册代码。通过以下代码更新 Execute
方法:
public void Execute(GeneratorExecutionContext context)
{
context.AddSource("AutoRegisterAttribute", SourceText.From(AttributeText, Encoding.UTF8));
if (!(context.SyntaxReceiver is MySyntaxReceiver receiver))
{
return;
}
CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
SyntaxTree attributeSyntaxTree =
CSharpSyntaxTree.ParseText(SourceText.From(AttributeText, Encoding.UTF8), options);
Compilation compilation = context.Compilation.AddSyntaxTrees(attributeSyntaxTree);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append(@"
using System;
using CodeGeneratorDemo.Client.Core;
namespace CodeGeneratorDemo.SourceGeneratorDemo
{
public class RegisterHelper
{
public static void RegisterServices()
{
");
// Get all the classes with the AutoRegisterAttribute
INamedTypeSymbol attributeSymbol =
compilation.GetTypeByMetadataName("CodeGeneratorDemo.SourceGeneratorDemo.AutoRegisterAttribute");
foreach (var candidateClass in receiver.CandidateClasses)
{
SemanticModel model = compilation.GetSemanticModel(candidateClass.SyntaxTree);
if (model.GetDeclaredSymbol(candidateClass) is ITypeSymbol typeSymbol &&
typeSymbol.GetAttributes().Any(x =>
x.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
{
stringBuilder.Append($@"
DiContainerMocker.RegisterService<I{candidateClass.Identifier.Text}, {candidateClass.Identifier.Text}>(new {candidateClass.Identifier.Text}());");
}
}
stringBuilder.Append(@"
}
}
}");
context.AddSource("RegisterServiceHelper", SourceText.From(stringBuilder.ToString(), Encoding.UTF8));
}
}
如果您不熟悉Roslyn,则这个方法可能看起来有点复杂。它使用Roslyn API来获取类的元数据-与Reflection类似。您可以检查文档以获取更多信息:
-
Work with syntax
[33] -
Work with semantics
[34] -
Explore code with the Roslyn syntax visualizer in Visual Studio
[35]
为了更好地检查项目中的语法树,您可以从 Visual Studio Installer
安装**.NET Compiler Platform SDK**,该工具为VS2019提供SyntaxVisualizer窗口。
一旦找到由 AutoRegister
属性修饰的类,就可以将注册实例的代码添加到源代码中。生成的代码将与原始代码一起编译。通过这种方式,我们避免了Reflection的昂贵成本并提高了性能。
最后,我们可以在客户端中调用生成的代码:
private static void SourceGeneratorSample()
{
Console.WriteLine("Here is the simple Source Generator sample:");
CodeGeneratorDemo.SourceGeneratorDemo.SpeakerHelper.SayHello();
Console.WriteLine();
Console.WriteLine("Here is the AutoRegisterAttribute Source Generator sample:");
CodeGeneratorDemo.SourceGeneratorDemo.RegisterHelper.RegisterServices();
}
您需要编译 CodeGeneratorDemo.SourceGeneratorDemo
项目,重新打开VS2019。然后您可以看到如下输出:
Here is the AutoRegisterAttribute Source Generator sample:
CodeGeneratorDemo.Client.Core.OrderService constructed.
CodeGeneratorDemo.Client.Core.OrderService has been registered for CodeGeneratorDemo.Client.Core.IOrderService.
CodeGeneratorDemo.Client.Core.ProductService constructed.
CodeGeneratorDemo.Client.Core.ProductService has been registered for CodeGeneratorDemo.Client.Core.IProductService.
如果您在 RegisterServices()
方法上按F12检查它的定义,可以发现生成的代码如下:
using System;
using CodeGeneratorDemo.SourceGeneratorDemo.Core;
namespace CodeGeneratorDemo.SourceGeneratorDemo
{
public class RegisterHelper
{
public static void RegisterServices()
{
DiContainerMocker.RegisterService(new ProductService());
DiContainerMocker.RegisterService(new OrderService());
}
}
}
这正是我们想要的。
很棒的事情是,如果在某个Sevice上删除或添加了 AutoRegister
Attribute,您将看到生成的代码将立即更新,无需重新编译项目!
如何调试 Source Generators
有时,我们需要调试Source Generators。如果仅在Source Generator中设置一个断点,您将发现它将无法工作。解决方案是在 Initialize
方法中附加调试器:
public void Initialize(GeneratorInitializationContext context)
{
#if DEBUG
if (!Debugger.IsAttached)
{
Debugger.Launch();
}
#endif
context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
}
然后,您可以通过设置断点来调试Source Generator。
如何处理复杂的模板代码?
在这两个示例中,我演示了如何使用Source Generators生成代码。我们在 Execute
方法中使用了原始字符串——看起来很丑。更好的方法是使用模板引擎。一种可能的选择是 Scriban
[36]
——一种用于.NET的快速,强大,安全和轻量级的脚本语言和引擎。因此,我们可以将模板存储在单独的文件中,这样项目会比较整洁。我不会深入探讨模板语法,因为它不在本文讨论范围之内。您可以在其GitHub存储库中找到更多信息。
使用场景
Microsoft提供了一个Source Generators cookbook。您可以在GitHub上找到它:https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md。您将看到Source Generators可以在许多情况下应用,尤其是替换Reflection或开发样板代码时。例如,某些JSON序列化经常使用动态分析,例如使用Reflection在运行时检查类型。源代码生成器可以在编译时生成静态序列化代码,以节省成本。您还可以访问其他文件(例如XML或JSON文件)来生成代码。
在GitHub上查找更多示例:https://github.com/dotnet/roslyn-sdk/tree/master/samples/CSharp/SourceGenerators。
小结
在本文中,我向您介绍了可用于在C#程序中生成代码的四种方式。它们可能适合不同的场景,因此我们需要比较每种方法并选择适当的方式。
场景 | 优点 | 缺点 | |
---|---|---|---|
Code Snippets | 以特定格式创建代码块,例如属性,方法和类等。 | 节省键入重复代码块的时间。 | 仅适用于特定格式。无法自动更新。 |
Reflection | 在运行时获取元数据,然后与类,属性,方法等进行交互。 | 在许多情况下功能强大且灵活。可以减少耦合。 | 昂贵的成本。潜在的性能问题。维护更复杂。 |
T4 Template | 用于生成一些样板代码。但是有时可以通过设计模式对其进行重构。 | 可以从其他文件读取数据。许多可用的控制块。可以生成静态代码而不会出现性能问题。 | 糟糕的编辑器支持。容易在模板中犯错误。 |
Source Generators | 可用于替换一些Reflection代码。在基于Roslyn的编译中生成静态代码。 | 没有性能问题。编译速度更快。支持智能感知。无法生成源代码时可以产生诊断信息。支持Partial Class或Partial Method。 | 工具需要改进。有点难以上手。 |
本文的重点是如何使用Source Generators-.NET 5中提供的新功能。它仍处于预览状态,因此我们可能很快会看到Microsoft的更多改进。我的期望是与VS2019更好地集成。现在的体验还不够好,因为我们必须反复重新打开VS。希望本文能帮助您节省C#开发的时间。如果您有任何想法,请随时发表您的评论。谢谢。
参考资料
C# Code Snippets: https://docs.microsoft.com/en-us/visualstudio/ide/visual-csharp-code-snippets?view=vs-2019&WT.mc_id=DT-MVP-5001643
INotifyPropertyChanged: https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?WT.mc_id=DT-MVP-5001643
Walkthrough: Create a code snippet: https://docs.microsoft.com/en-us/visualstudio/ide/walkthrough-creating-a-code-snippet?view=vs-2019&WT.mc_id=DT-MVP-5001643
Code snippet functions: https://docs.microsoft.com/en-us/visualstudio/ide/code-snippet-functions?view=vs-2019&WT.mc_id=DT-MVP-5001643
How to: Distribute code snippets: https://docs.microsoft.com/en-us/visualstudio/ide/how-to-distribute-code-snippets?view=vs-2019&WT.mc_id=DT-MVP-5001643
ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-5.0&WT.mc_id=DT-MVP-5001643
Entity Framework Core: https://docs.microsoft.com/en-us/ef/core/?WT.mc_id=DT-MVP-5001643
类型的: https://docs.microsoft.com/en-us/dotnet/api/system.type?WT.mc_id=DT-MVP-5001643
System.Reflection: https://docs.microsoft.com/en-us/dotnet/api/system.reflection?WT.mc_id=DT-MVP-5001643
System.Type: https://docs.microsoft.com/en-us/dotnet/api/system.type?WT.mc_id=DT-MVP-5001643
Assembly: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly?WT.mc_id=DT-MVP-5001643
Module: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.module?WT.mc_id=DT-MVP-5001643
ConstructorInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.constructorinfo?WT.mc_id=DT-MVP-5001643
MethodInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodinfo?WT.mc_id=DT-MVP-5001643
FieldInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.fieldinfo?WT.mc_id=DT-MVP-5001643
EventInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.eventinfo?WT.mc_id=DT-MVP-5001643
PropertyInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.propertyinfo?WT.mc_id=DT-MVP-5001643
ParameterInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.parameterinfo?WT.mc_id=DT-MVP-5001643
CustomAttributeData: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.customattributedata?WT.mc_id=DT-MVP-5001643
Reflection in .NET: https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/reflection?WT.mc_id=DT-MVP-5001643
Viewing Type Information: https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/viewing-type-information?WT.mc_id=DT-MVP-5001643
Dynamically Loading and Using Types: https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/dynamically-loading-and-using-types?WT.mc_id=DT-MVP-5001643
DevArt T4 Editor: https://www.devart.com/t4-editor/
Code Generation and T4 Text Templates: https://docs.microsoft.com/en-us/visualstudio/modeling/code-generation-and-t4-text-templates?view=vs-2019&WT.mc_id=DT-MVP-5001643
Walkthrough: Generate Code by using Text Templates: https://docs.microsoft.com/en-us/visualstudio/modeling/walkthrough-generating-code-by-using-text-templates?view=vs-2019&WT.mc_id=DT-MVP-5001643
Run-Time Text Generation with T4 Text Templates: https://docs.microsoft.com/en-us/visualstudio/modeling/run-time-text-generation-with-t4-text-templates?view=vs-2019&WT.mc_id=DT-MVP-5001643
T4 Text Template Directives: https://docs.microsoft.com/en-us/visualstudio/modeling/t4-text-template-directives?view=vs-2019&WT.mc_id=DT-MVP-5001643
Text Template Control Blocks: https://docs.microsoft.com/en-us/visualstudio/modeling/text-template-control-blocks?view=vs-2019&WT.mc_id=DT-MVP-5001643
Guidelines for Writing T4 Text Templates: https://docs.microsoft.com/en-us/visualstudio/modeling/guidelines-for-writing-t4-text-templates?view=vs-2019&WT.mc_id=DT-MVP-5001643
.NET 5 SDK: https://dotnet.microsoft.com/download/dotnet/5.0
Attribute: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/?WT.mc_id=DT-MVP-5001643
Roslyn: https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/?WT.mc_id=DT-MVP-5001643
Work with syntax: https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/work-with-syntax?WT.mc_id=DT-MVP-5001643
Work with semantics: https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/work-with-semantics?WT.mc_id=DT-MVP-5001643
Explore code with the Roslyn syntax visualizer in Visual Studio: https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/syntax-visualizer?tabs=csharp&WT.mc_id=DT-MVP-5001643
Scriban: https://github.com/scriban/scriban