在 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已經為我們提供了大量的內置的代碼片段,如prop,if,while,for,try,您可以在這里找到所有的默認代碼片段列表: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.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"?#> <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文件,因此我們需要更新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<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
總結
以上是生活随笔為你收集整理的在 C# 中生成代码的四种方式——包括.NET 5中的Source Generators的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 为你的项目启用可空引用类型
- 下一篇: 使用 C# 9 的records作为强类