C# 強大的新特性 Source Generator
Intro
微軟在 .NET 5 中引入了 Source Generator 的新特性,利用 Source Generator 我們可以在應用編譯的期間根據當前編譯信息動態生成代碼,而且可以在我們的 C# 代碼中直接引用動態生成的代碼,從而大大減少重復代碼。
What
源代碼生成器(Source Generators) 是一段在編譯過程中運行的代碼,可以根據程序中的代碼來生成其他文件,這些文件可以與其余代碼一起編譯。
使用 Source Generators,可以做到這些事情:
-
獲取一個 Compilation 對象,這個對象表示了所有正在編譯的用戶代碼,你可以從中獲取 AST 和語義模型等信息
-
可以向 Compilation 對象中插入新的代碼,讓編譯器連同已有的用戶代碼一起編譯
Source Generators 作為編譯過程中的一個階段執行:
編譯運行 -> [分析源代碼 -> 生成新代碼] -> 將生成的新代碼添加入編譯過程 -> 編譯繼續。
上述流程中,中括號包括的內容即為 Source Generators 所參與的階段和能做到的事情,如下圖所示。
source generator
Why
編譯時反射
拿 ASP.NET Core 舉例,啟動一個 ASP.NET Core 應用時,首先會通過運行時反射來發現 Controllers、Services 等的類型定義,然后在請求管道中需要通過運行時反射獲取其構造函數信息以便於進行依賴注入。然而運行時反射開銷很大,即使緩存了類型簽名,對於剛剛啟動后的應用也無任何幫助作用,而且不利於做 AOT 編譯。
Source Generators 將可以讓 ASP.NET Core 所有的類型發現、依賴注入等在編譯時就全部完成並編譯到最終的程序集當中,最終做到 0 運行時反射使用,不僅利於 AOT 編譯,而且運行時 0 開銷。
除了上述作用之外,gRPC 等也可以利用此功能在編譯時織入代碼參與編譯,不需要再利用任何的 MSBuild Task 做代碼生成啦!
另外,甚至還可以讀取 XML、JSON 直接生成 C# 代碼參與編譯,DTO 編寫全自動化都是沒問題的。
AOT 編譯
Source Generators 的另一個作用是可以幫助消除 AOT 編譯優化的主要障礙。
許多框架和庫都大量使用反射,例如 System.Text.Json
、System.Text.RegularExpressions
、ASP.NET Core 和 WPF 等等,它們在運行時從用戶代碼中發現類型。這些非常不利於 AOT 編譯優化,因為為了使反射能夠正常工作,必須將大量額外甚至可能不需要的類型元數據編譯到最終的原生映像當中。
有了 Source Generators 之后,只需要做編譯時代碼生成便可以避免大部分的運行時反射的使用,讓 AOT 編譯優化工具能夠更好的運行。
How it works
Source Generator 的工作方式和靜態分析器(Analyzer)類似,它是 Analyzer 的補充,在 Analyzer 的基礎上增加了生成源代碼部分的功能
generated code
正常的話可以在項目的 Dependencies
/Analyzers
找到自己的 Source Generator 項目以及動態生成的代碼(上圖來自 Roslyn 官方的示例(https://github.com/dotnet/roslyn-sdk/tree/main/samples/CSharp/SourceGenerators)在 VS 中的截圖)
Sample
來看一個簡單的示例吧,示例主要項目結構如下:
GeneratedDemo
是引用 Source Generator 的項目,動態生成的代碼也是生成在這個項目里
GeneratedDemo1
是引用 GeneratedDemo
項目的項目,可以不必太關心,只是想驗證一下其他項目里是否也可以直接調用生成的代碼(毫無疑問是可以的
Generators
是我們 Source Generator 的項目,動態生成代碼的邏輯都在這個項目里
來看一個 Hello world 示例
首先 Generators
項目需要添加對 Microsoft.CodeAnalysis.CSharp
的引用,官方示例中還要引用 Microsoft.CodeAnalysis.Analyzers
,實際測試下來可以不需要引用,因為 Microsoft.CodeAnalysis.CSharp
的依賴項 Microsoft.CodeAnalysis.Common
已經有對 Microsoft.CodeAnalysis.Analyzers
的依賴,如果要使用最新版的也可以添加對最新版本的 Microsoft.CodeAnalysis.Analyzers
的依賴
實現一個簡單的 Generator,我們需要實現 ISourceGenerator
接口並且添加 [Generator]
Attribute 標記,下面是一個簡單的示例:
-
[Generator]
-
public class HelloGenerator : ISourceGenerator
-
{
-
public void Initialize(GeneratorInitializationContext context)
-
{
-
// for debugging
-
// if (!Debugger.IsAttached) Debugger.Launch();
-
}
-
-
public void Execute(GeneratorExecutionContext context)
-
{
-
var code = @"namespace HelloGenerated
-
{
-
public class HelloGenerator
-
{
-
public static void Test() => System.Console.WriteLine(""Hello Generator"");
-
}
-
}";
-
context.AddSource(nameof(HelloGenerator), code);
-
}
-
}
ISourceGenerator
有兩個接口:
-
Initialize
,在Initialize
方法中, 我們可以通過RegisterForSyntaxNotifications
注冊自己的語法接收器來篩選自己關心的語法節點,也可以通過RegisterForPostInitialization
注冊初始化完成之后的回調,也可以通過CancellationToken
來判斷是否停止初始化過程,如果初始化邏輯比較復雜耗時較長的時候可以考慮判斷CancellationToken
來檢測用戶用戶是否取消了編譯 -
Execute
,我們通常生成代碼都是在這個方法中處理的,這個方法有更多的信息,我們可以獲取到當前的編譯信息,我們可以通過AddSource
方法來添加我們自定義代碼,可以通過SyntaxReceiver
/SyntaxContextReceiver
來獲取我們在Initialize
方法中注冊的自定義語法接收器,同樣的我們也可以根據CancellationToken
來判斷是否停止編譯過程
然后我們需要檢查一下項目引用,在引用 Generators
項目的時候需要設置一下項目 OutputItemType="Analyzer"
,項目引用示例如下:
-
<ProjectReference Include= "..\Generators\Generators.csproj"
-
OutputItemType= "Analyzer" />
上面的示例就是一個簡單的生成一個 HelloGenerator
類,這個類命名空間是 HelloGenerated
,有一個 Test
的靜態方法,然后我們就可以在我們的代碼里通過 HelloGenerated.HelloGenerator.Test();
引用這個方法了
運行 GeneratedDemo
項目,就可以得到下面的輸出結果了~
上面這個只是一個很簡單的直接添加一段代碼的示例,我們也可以根據編譯信息進行動態生成,再來看一個示例吧
-
[Generator]
-
public class ModelGenerator : ISourceGenerator
-
{
-
public void Initialize(GeneratorInitializationContext context)
-
{
-
// Debugger.Launch();
-
context.RegisterForSyntaxNotifications(() => new CustomSyntaxReceiver());
-
}
-
-
public void Execute(GeneratorExecutionContext context)
-
{
-
var codeBuilder = new StringBuilder(@"
-
using System;
-
using WeihanLi.Extensions;
-
-
namespace Generated
-
{
-
public class ModelGenerator
-
{
-
public static void Test()
-
{
-
Console.WriteLine(""-- ModelGenerator --"");
-
");
-
-
if (context.SyntaxReceiver is CustomSyntaxReceiver syntaxReceiver)
-
{
-
foreach ( var model in syntaxReceiver.Models)
-
{
-
codeBuilder.AppendLine($@ " ""{model.Identifier.ValueText} Generated"".Dump();");
-
}
-
}
-
-
codeBuilder.AppendLine( " }");
-
codeBuilder.AppendLine( " }");
-
codeBuilder.AppendLine( "}");
-
var code = codeBuilder.ToString();
-
context.AddSource(nameof(ModelGenerator), code);
-
}
-
}
-
-
internal class CustomSyntaxReceiver : ISyntaxReceiver
-
{
-
public List<ClassDeclarationSyntax> Models { get; } = new();
-
-
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
-
{
-
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax)
-
{
-
Models.Add(classDeclarationSyntax);
-
}
-
}
-
}
增加了這個 ModelGenerator
之后,我在 GeneratedDemo
項目里增加了一個 User
類來測試,User
類很簡單,就是一個很普通的 Model
-
public class User
-
{
-
public int Id { get; set; }
-
-
public string Name { get; set; }
-
}
上面這個 Generator,稍微復雜一些,增加了依賴項,依賴項的處理需要修改項目文件,項目文件修改如下:
-
<PropertyGroup>
-
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
-
</PropertyGroup>
-
<ItemGroup>
-
<PackageReference Include= "WeihanLi.Common" Version="1.0.46" GeneratePathProperty="true" />
-
</ItemGroup>
-
<Target Name= "GetDependencyTargetPaths">
-
<ItemGroup>
-
<TargetPathWithTargetPlatformMoniker Include= "$(PKGWeihanLi_Common)\lib\netstandard2.0\WeihanLi.Common.dll" IncludeRuntimeDependency="false" />
-
</ItemGroup>
-
</Target>
需要增加一個 Target
來幫助編譯器找到相應的依賴,不知道為什么它自己找不到。。感覺后面應該可以優化一下可以自己解析依賴,希望后面的版本能夠解決這個依賴解析的問題,不需要開發者自己配置使用起來就比較方便了
執行 Generated.ModelGenerator.Test();
來調用我們動態生成的代碼,運行結果如下:
可以看到我們動態生成的代碼正常工作了
示例 Github 地址:https://github.com/WeihanLi/SamplesInPractice/tree/master/SourceGeneratorSample
Tips
在實際使用的過程中,還是遇到了很多問題,VS 的支持並不是特別的好,有時候 Source Generator 生成的代碼 VS 並不能夠很好的感知,可能會出現你用 dotnet cli 編譯可以通過但是 VS 編譯會報錯,會出現找不到生成的代碼類似的錯誤,所以比較推薦使用 dotnet cli 進行編譯,使用 VS 進行調試
如果需要調試 Source Generator 可以在 Generator 代碼里添加 Debugger.Launch()
來請求一個 Debugger,這時我們就可以選擇 VS 來調試我們的 Generator 代碼了
正常的話可以在 VS 里依賴中的 Analyzer 里看到生成的代碼,但是有時候 VS 智障的時候就看不到,為了比較一致的開發調試體驗,推薦在引用 Source Generator 的項目文件中添加 <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
來在項目的 obj/Debug/$(TargetFramework)/generated
目錄下生成實際生成的源代碼,如下圖所示,這樣我們就可以比較方便的知道生成的代碼是否符合我們的預期,另外也可以通過反編譯生成的 dll 來查看生成的代碼
在引用 Source Generator 的項目中引用的時候 ReferenceOutputAssembly="false"
這個是否要設置要根據自己的項目實際情況來定,如果你的 Source Generator 項目里只有一個 Generator,沒有別的類被引用可以設置,如果有則不能有這個配置,否則可能會導致編譯失敗
More
VS 的支持感覺還是有點欠缺(也可能只是我的 VS 有問題)希望以后會更好,剛開始折騰了兩天 VS,VS 還修復了兩次,Resharper 也重裝了一下,最后還是通過 dotnet cli 去編譯了,感謝馮輝大佬的幫助,否則還要浪費更多的時間了
目前 Source Generator 對於依賴項的支持還是比較弱的,希望后面版本的支持會變得好用,增加對於依賴項的依賴項的加載支持
現在已經有很多基於 Source Generator 的項目了,依賴注入、Mock、Mapper 等等,除了官方的示例,也可以從 Github 上這個項目 https://github.com/amis92/csharp-source-generators 了解更多,文末有一些不錯的參考資料可以了解學習,Channel 9 上也有一個介紹視頻,傳送門(https://channel9.msdn.com/Shows/On-NET/C-Source-Generators)
我嘗試用 Source Generator 在我們項目中代替了原來的 T4,從原來的比較手動的方式開發時生成,變成自動的在編譯時生成,將會在下一篇文章中詳細介紹
References
-
https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/
-
https://www.cnblogs.com/hez2010/p/12810993.html
-
https://www.cnblogs.com/yyfh/p/14545758.html
-
https://www.cnblogs.com/kewei/p/14322474.html
-
https://github.com/amis92/csharp-source-generators
-
https://github.com/dotnet/roslyn-sdk/tree/main/samples/CSharp/SourceGenerators
-
https://channel9.msdn.com/Shows/On-NET/C-Source-Generators
-
https://github.com/WeihanLi/SamplesInPractice/tree/master/SourceGeneratorSample