22

在 C# 中生成代码的四种方式——包括.NET 5中的Source Generators

 4 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzAwNTMxMzg1MA%3D%3D&%3Bmid=2654082368&%3Bidx=1&%3Bsn=2ddbe2243eaaa6e10d34dda7db523981
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Microsoft在最新的C#版本中引入了Source Generator。这是一项新功能,可以让我们在代码编译时生成源代码。在本文中,我将介绍四种C#中的代码生成方式,以简化我们的日常工作。然后,您可以视情况选择正确的方法。

在 .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已经为我们提供了大量的内置的代码片段,如 propifwhilefortry ,您可以在这里找到所有的默认代码片段列表: C# Code Snippets [1] 。

Code snippets 的好处是您可以替换参数。例如,当我们将MVVM模式用于UWP / Xamarin / WPF应用程序时,经常需要在实现 INotifyPropertyChanged [2] 接口的类中创建属性。如果您使用 MvvmCross 框架,它可能看起来像这样:

private ObservableCollection<Comment> _commentList;
public ObservableCollection<Comment> CommentList
{
get => _commentList;
set => SetProperty(ref _commentList, value);
}

我们不想复制/粘贴然后更改变量名,所以我创建了一个 Code snippet 来简化工作。创建一个名为 myMvvm.snippet 的新文件,然后复制并粘贴以下代码:

<?xml version="1.0" encoding="utf-8"?>
<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 中,我们使用 <Shortcut> 指定快捷方式 mvxprop ,并使用 <Declarations> 声明一些参数。例如,我们声明了一个名为的参数 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<Type> 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

ramameZ.png!mobile

您可以在 CodeGeneratorDemo.T4TemplateDemo 项目中找到示例,包含两个T4 Template: RunTimeTextTemplateDemo.ttDesignTimeTextTemplateDemo.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" #>
<html><body>
<h1>Sales for Previous Month</h2>
<table>
<# for (int i = 1; i <= 10; i++)
{ #>
<tr><td>Test name <#= i #> </td>
<td>Test value <#= i * i #> </td> </tr>
<# } #>
</table>
This report is Company Confidential.
</body></html>

每次保存模板时,它将更新生成的文件。在客户端代码中,我们可以这样调用:

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<#=model#>(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,只需调试项目,因为它会在程序编译时运行。

N7NbqeB.png!mobile

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的示意图:

73Ize2.png!mobile

我们需要知道的一件事是, 源生成器只能向代码中添加内容,而不能更改任何现有代码 。让我们来看一个例子。

第一个 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文件,因此我们需要更新 OutputItemTypeReferenceOutputAssembly 的值,如上所示。

在客户端代码中添加代码:

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无法识别生成的方法或类,只要构建正确运行即可。

Mb6R3aF.png!mobile

如果在客户端代码中按一下 F12 以检查 SayHello() 方法,您将看到生成的文件,该文件显示此文件无法编辑:

uyieUju.png!mobile

您可能很好奇文件在哪里。如果要查看实际生成的文件,可以将以下部分添加到 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
{
}
}

还有一些实现类,例如 OrderServiceProductService ,由 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<TInterface, TImplementation>(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<ClassDeclarationSyntax> CandidateClasses { get; } = new List<ClassDeclarationSyntax>();

/// <summary>
/// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
/// </summary>
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窗口。

Jbeia2y.png!mobile

一旦找到由 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<IProductService, ProductService>(new ProductService());
DiContainerMocker.RegisterService<IOrderService, OrderService>(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#开发的时间。如果您有任何想法,请随时发表您的评论。谢谢。

参考资料

[1]

C# Code Snippets: https://docs.microsoft.com/en-us/visualstudio/ide/visual-csharp-code-snippets?view=vs-2019&WT.mc_id=DT-MVP-5001643

[2]

INotifyPropertyChanged: https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?WT.mc_id=DT-MVP-5001643

[3]

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

[4]

Code snippet functions: https://docs.microsoft.com/en-us/visualstudio/ide/code-snippet-functions?view=vs-2019&WT.mc_id=DT-MVP-5001643

[5]

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

[6]

ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-5.0&WT.mc_id=DT-MVP-5001643

[7]

Entity Framework Core: https://docs.microsoft.com/en-us/ef/core/?WT.mc_id=DT-MVP-5001643

[8]

类型的: https://docs.microsoft.com/en-us/dotnet/api/system.type?WT.mc_id=DT-MVP-5001643

[9]

System.Reflection: https://docs.microsoft.com/en-us/dotnet/api/system.reflection?WT.mc_id=DT-MVP-5001643

[10]

System.Type: https://docs.microsoft.com/en-us/dotnet/api/system.type?WT.mc_id=DT-MVP-5001643

[11]

Assembly: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly?WT.mc_id=DT-MVP-5001643

[12]

Module: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.module?WT.mc_id=DT-MVP-5001643

[13]

ConstructorInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.constructorinfo?WT.mc_id=DT-MVP-5001643

[14]

MethodInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.methodinfo?WT.mc_id=DT-MVP-5001643

[15]

FieldInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.fieldinfo?WT.mc_id=DT-MVP-5001643

[16]

EventInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.eventinfo?WT.mc_id=DT-MVP-5001643

[17]

PropertyInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.propertyinfo?WT.mc_id=DT-MVP-5001643

[18]

ParameterInfo: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.parameterinfo?WT.mc_id=DT-MVP-5001643

[19]

CustomAttributeData: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.customattributedata?WT.mc_id=DT-MVP-5001643

[20]

Reflection in .NET: https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/reflection?WT.mc_id=DT-MVP-5001643

[21]

Viewing Type Information: https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/viewing-type-information?WT.mc_id=DT-MVP-5001643

[22]

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

[23]

DevArt T4 Editor: https://www.devart.com/t4-editor/

[24]

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

[25]

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

[26]

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

[27]

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

[28]

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

[29]

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

[30]

.NET 5 SDK: https://dotnet.microsoft.com/download/dotnet/5.0

[31]

Attribute: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/attributes/?WT.mc_id=DT-MVP-5001643

[32]

Roslyn: https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/?WT.mc_id=DT-MVP-5001643

[33]

Work with syntax: https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/work-with-syntax?WT.mc_id=DT-MVP-5001643

[34]

Work with semantics: https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/work-with-semantics?WT.mc_id=DT-MVP-5001643

[35]

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

[36]

Scriban: https://github.com/scriban/scriban


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK