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

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 domainvar availableSpeakers = ReflectionHelper.GetAvailableSpeakers();foreach (var availableSpeaker in availableSpeakers){// Create the instance of the typevar speaker = Activator.CreateInstance(availableSpeaker);// Get the property info of the given property namePropertyInfo namePropertyInfo = availableSpeaker.GetProperty("Name");// Then you can get the value of the propertyvar name = namePropertyInfo?.GetValue(speaker)?.ToString();Console.WriteLine($"I am {name}");// Invoke the method of the instanceConsole.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.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,只需调试项目,因为它会在程序编译时运行。

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

如果在客户端代码中按一下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{}
}

还有一些实现类,例如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 generationif (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 AutoRegisterAttributeINamedTypeSymbol 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<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 DEBUGif (!Debugger.IsAttached){Debugger.Launch();}
#endifcontext.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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/305655.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

powercfg -h off_驭鲛记的主演会是谁?肖战关系特别好的艺人朋友呢?白敬亭和吴映洁有没有故事啊?高伟光是不是隐婚生子了?讲讲管h和马司令呗?...

近期后台提问的比较多&#xff0c;没被翻牌的小可爱们不要着急&#xff0c;我会尽力把大家的问题都照顾到&#xff0c;笔芯1. 扒扒&#xff0c;想知道华策驭鲛记的主演会是谁&#xff1f;主演还没定&#xff0c;女主在接触热巴&#xff0c;男主还没接触&#xff0c;这个戏明年才…

使用 C# 9 的records作为强类型ID - JSON序列化

使用 C# 9 的records作为强类型ID - 路由和查询参数在本系列的上一篇文章中使用 C# 9 的records作为强类型ID - 路由和查询参数&#xff0c;我们注意到强类型ID的实体&#xff0c;序列化为 JSON 的时候报错了&#xff0c;就像这样&#xff1a;{"id": {"value&qu…

HP LaserJet 1010卡纸解决方法

HP LaserJet 1010 系列打印机在打印过程中出现卡纸多由以下原因造成&#xff1a;1、纸盒里放入了过多的纸张或纸张位置没有放好。2、打印时使用的介质类型超出打印机的支持范围。常见卡纸位置&#xff1a;1 、硒鼓下方&#xff1b;2 、进纸口&#xff1b;3 、出纸口图 2&#x…

HP产品选件查询网站

[url]http://h18000.www1.hp.com/products/quickspecs/ProductBulletin.html#intro[/url][url]http://h18006.www1.hp.com/products/quickspecs/Division/12175.html[/url]所有的可通地此链接来查询DL380G5:[url]http://h18004.www1.hp.com/products/quickspecs/12477_div/1247…

如何使用 C# 中的 ValueTuple

Tuple 是一种数据结构&#xff0c;它由一个有序的、有限的、大小固定的、不可变的异构元素的序列组成&#xff0c;当我们说 Tuple 中的元素不可变&#xff0c;意味着其中的元素不能进行修改。ValueTuple 是在 C# 7 中被引入&#xff0c;它主要用来解决 Tuple 的两个问题。解决语…

tutte定理证明hall定理_人教社课本现低级错误?“爱因斯坦用相对论证明勾股定理”...

南方加客户端南方加客户端6月18日消息&#xff0c;近日&#xff0c;有网友在网上发帖称&#xff0c;人教版八年级下册数学自读课本中有关“爱因斯坦证明勾股定理”的内容疑似出现错误&#xff0c;此事引发网友关注&#xff0c;目前在社交平台上发酵。网友上传的课本图片据网友上…

在 “相对” 高薪面前,任何的喊冤叫屈都是苍白无力的

2021年刚开始&#xff0c;我的朋友圈就被一桩接着一桩的 “噩耗” 连番轰炸。1月1日&#xff0c;曾在《巴啦啦小魔仙》中饰演 “凌美琪” 的孙侨潞不幸去世&#xff0c;年仅25岁&#xff0c;死因是常年熬夜&#xff0c;再加上饮酒过量而导致的猝死。1月3日&#xff0c;我在网上…

在SQLSERVER企业管理器中如何创建触发器

下面将分别介绍在MS SQLServer 中如何用SQL Server 管理工具Enterprise Manager 和Transaction_SQL 来创建触发器。在创建触发器以前必须考虑到以下几个方面&#xff1a; CREATE TRIGGER 语句必须是批处理的第一个语句; 表的所有者具有创建触发器的缺省权限,表的所有者不能把该…

srv.sys蓝屏解决补丁_Win10 补丁 KB4556799 导致部分用户蓝屏死机和网络问题

IT之家5月26日消息 Windows 10 补丁 KB4556799对某些配置造成了许多新问题。除了音频问题&#xff0c;临时用户配置文件和FPS下降之外&#xff0c;Windows 10最新累积更新还导致某些用户出现蓝屏死机、崩溃和网络问题。与Windows 10更新一样&#xff0c;用户经常遇到一系列不同…

IdentityServer4(六)授权码流程原理之SPA

在【One by One系列】IdentityServer4&#xff08;四&#xff09;授权码流程中提过一句&#xff1a;“为了安全&#xff0c;IdentityServer4是带有PKCE支持的授权码模式”我们来回顾一下授权码流程&#xff08;A&#xff09;用户访问客户端&#xff0c;后者将前者导向认证服务器…

适合手机端的ckeditor样式_抖音运营干货(三):9款手机视频剪辑APP,让你轻松玩转后期!...

很多朋友想开始用手机拍视频&#xff0c;可能不知道如何剪辑&#xff01;本文将给大家介绍几款好用又方便的手机剪辑短视频工具&#xff0c;即便是零基础&#xff0c;用下面这些工具&#xff0c;你也可以轻松开始剪辑短视频。选择一款实用好用的剪辑工具很重要&#xff0c;工具…

来吧,是时候升级您的领英技术档案了

阅读此文需要2分钟&#xff08;文末有惊喜&#xff09;LinkedIn的应用之广超乎你的想象&#xff0c;包括社会招聘、公关、社群建设、销售、社交媒体营销&#xff08;包括社交广告&#xff09;以及员工宣传。LinkedIn档案不是一份简历&#xff0c;而是集客式营销&#xff08;inb…

c语言查单词小程序,【附源码】小程序初窥之简单查单词

新年假期百无聊赖&#xff0c;于是就看了一下微信小程序的开发方法&#xff0c;花了两天时间入了个门&#xff0c;这里记录一下。阅读之前&#xff0c;先确定你知道基本的 htmlcssjs 语法&#xff0c;这样就能更好地和我一样&#xff0c;以一个新手的视角来理解小程序。目标目标…

python连接mysql_Python爬虫进阶教程(八):MySQL 数据库连接

PyMySQL介绍PyMySql包含一个纯python的MySQL客户端库。PyMySQL的目标是成为MySQLdb的替代品&#xff0c;并在CPython、PyPy和IronPython上工作。版本要求python 下列之一CPython > 2.6 or > 3.3PyPy > 4.0IronPython 2.7mysql 下列之一MySQL > 4.1 (tested with on…

使用 gRPCurl 调试.NET 5的gPRC服务

介绍你用过 Curl 吗&#xff1f;这个工具允许你通过 http 来发送数据&#xff0c;现在有一个适用于gGRPC的工具&#xff0c;gRPCurl&#xff0c;在本文中&#xff0c;我将介绍如何下载安装这个工具&#xff0c;然后通过这个工具调试我们.NET 5上面的gGRC程序。安装 gRPCurlgRPC…

此 sqltransaction 已完成;它再也无法使用_手把手教你如何修眉毛,学会再也不用花钱去美容院了...

最近看到有网友在评论中询问怎么修眉毛&#xff0c;刚好小编我对修眉有一点研究&#xff0c;可能比不上专业修眉的&#xff0c;但是最少能看不是&#xff0c;今天拿出来献给大家&#xff0c;希望各位自己学会如何修眉毛&#xff0c;那样的话以后就再也不用花钱去美容院了。在此…

国产OS推广应从娃娃和体制内双管齐下

一直以来&#xff0c;国内桌面操作系统被微软垄断。究其根源&#xff0c;既有微软技术更成熟&#xff0c;软件生态丰富、服务完善、商业化水平更好等因素之外&#xff0c;也有老百姓习惯于使用Windows等因素。老百姓之所以习惯于Windows&#xff0c;则是因为破解版横行和微软早…

c语言铁路托运行李费用图,3.为铁路部门编写计算运费的程序。假设铁路托运行李,规定每张客票托运费计算方法是:行李重量不超过50kg...

满意答案wodfsdfeqd81推荐于 2018.10.08采纳率&#xff1a;51% 等级&#xff1a;12已帮助&#xff1a;5850人using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace ConsoleApplication40{class Program{static void Main(string[]…

ufo帧率测试网站_一加7游戏续航测试 满电开始玩猜猜能玩多久

业界顶级的90Hz刷新率屏幕打造沉浸式视觉体验&#xff1b;骁龙855移动平台加强性能输出&#xff1b;更加炫酷的曲面屏设计...采用无刘海式真全面屏&#xff0c;屏占比屏幕赠大的同时&#xff0c;电池容量也进一步得到提升。对于省电优化能力卓尔不群的一加7 Pro&#xff0c;让手…

并行模型Actor

并行开发时经常需要关注加锁和原子操作等一系列线程问题&#xff0c;而Actor模型内部状态由它自己维护&#xff0c;内部数据只能自己修改&#xff0c;因此Actor不需要过多关注线程问题。Actor模型Actor由状态&#xff08;State&#xff09;、邮箱&#xff08;Mailbox&#xff0…