最近迷上 C# 原始碼產生器 (Source Generators) 這門相當新穎的技術,跟以往常用的 T4 (Text Template Transformation Toolkit) 產生器技術不太一樣,這個 Source Generators 是屬於 Roslyn 編譯器的技術之一,讓你在專案建置的過程中,可以對正在編譯的 C# 原始碼進行增補,動態加入「額外」的原始碼,最後再編譯在一起,非常神奇又實用的技術,讓人非常有想像空間! 👍
區別 T4 與 Source Generators 的差異
從概念上來說,這兩個「程式碼產生器」技術相當類似,不過執行方式卻完全不同:
-
T4
主要是內嵌在 Visual Studio 中的一門技術,打從 Visual Studio 2005 開始就存在,不過這套產品一直不受微軟愛戴,直到 Visual Studio 2019 都還缺乏很基本的語法高量功能,都要靠擴充套件才能顯示語法高量,而且還很容易有 Bug 出現,尤其是使用「深色」模式的布景主題。使用 T4 技術來產生任意程式碼有個好處,他可以讓你在 Visual Studio 開發工具裡自動產生任意以「文字」為主的檔案內容,可以是 C#、可以是 VB、也可以是 XML 檔案,可以說是非常靈活。
-
Source Generators
主要執行在 Roslyn 編譯器的管道中 (compiler pipeline),所以產生器只能執行在 C# 編譯時期,你可以透過 SyntaxTree 讀取與分析目前專案中所有 C# 程式碼,你可以在分析完原始碼之後,自行產生「額外」的新程式碼,但是這份「新的程式碼」並不會出現在專案的原始碼中,而是在編譯時期自動產生的暫時程式碼,他會被編譯到你最終的 Assembly 組件裡。所以,你不能拿 Source Generators 來做一些跟 C# 編譯無關的事情(雖然你也可以這麼做,因為執行的其實也是 C# 程式,你想寫入任何檔案也是有可能的)。
由於 Source Generators 跟 Roslyn 相關,跟開發工具比較沒有直接關係,所以無論你用 Visual Studio 2019, Visual Studio Code 或使用 .NET CLI 建置專案,都可以自動利用 Source Generators 產生程式碼!
以下我將會使用 .NET 5 (SDK version 5.0.100
) CLI 搭配 Visual Studio Code 來撰寫一份最簡單的 Source Generators 實作。
建立 Source Generators 專案
你的任何一個 .NET Core 專案都可以加入任意 Source Generators 專案,不過有一些需要注意的地方,它並不是很單純的寫一個 Class 就可以完成 Source Generator 開發!
-
建立一個 Console 應用程式 (TFM: net5.0
)
dotnet new console -n ConsoleApp1
-
建立一個 Class library 類別庫專案 (TFM: netstandard2.0
)
dotnet new classlib -n MySourceGenerator
-
使用 Visual Studio Code 開啟 ConsoleApp1 並加入 MySourceGenerator 資料夾到工作區
加入成功後的畫面如下:
建議可以將多專案的工作區儲存起來,方便下次開啟:
-
將 MySourceGenerator 專案加入成為 ConsoleApp1
的專案參考 (Project Reference)
dotnet add reference ..\MySourceGenerator\MySourceGenerator.csproj
事實上這個命令會在 ConsoleApp1\ConsoleApp1.csproj
加入以下 <ItemGroup>
定義:
<ItemGroup>
<ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj" />
</ItemGroup>
不過,這個設定必須進行微調,請調整成以下設定,這才能真正成為一個有效的 Source Generator 專案參考:
<ItemGroup>
<ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
這裡的 OutputItemType="Analyzer"
用來設定 MySourceGenerator
專案是一個「分析器」專案。而 ReferenceOutputAssembly="false"
則是設定該專案不會成為該專案的參考組件。詳見 Common MSBuild project items 文件。
上述這四個步驟,是相當重要的初步設定,尤其是最後一步,沒有正確設定的話 Source Generators 是完全無法執行的!
撰寫基礎 Source Generator 類別
以下步驟都是在 MySourceGenerator
專案下進行設定:
-
請將 Class1.cs
更名為 MySourceGenerator.cs
,並將內容修改為以下內容:
using System;
namespace MySourceGenerator
{
public class MySourceGenerator
{
}
}
-
加入 Microsoft.CodeAnalysis.CSharp 與 Microsoft.CodeAnalysis.Analyzers 套件
dotnet add package Microsoft.CodeAnalysis.CSharp
dotnet add package Microsoft.CodeAnalysis.Analyzers
-
替 MySourceGenerator
類別套用 [Generator]
Attribute 與實作 ISourceGenerator 介面
using System;
using Microsoft.CodeAnalysis;
namespace MySourceGenerator
{
[Generator]
public class MySourceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
}
public void Initialize(GeneratorInitializationContext context)
{
}
}
}
這裡的 MySourceGenerator
類別已經設定完成,當 ConsoleApp1
專案再進行建置時,該類別就會自動執行,先執行 Initialize()
方法,再執行 Execute()
方法!
-
加入「程式碼產生器」到 Execute()
方法中 (記得加入必要的 using
匯入命名空間)
我直接提供官方提供的 Source Generators 範例程式來進行實作,請參考 HelloWorldGenerator.cs 檔案:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace MySourceGenerator
{
[Generator]
public class MySourceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// begin creating the source we'll inject into the users compilation
StringBuilder sourceBuilder = new StringBuilder(@"
using System;
namespace HelloWorldGenerated
{
public static class HelloWorld
{
public static void SayHello()
{
Console.WriteLine(""Hello from generated code!"");
Console.WriteLine(""The following syntax trees existed in the compilation that created this program:"");
");
// using the context, get a list of syntax trees in the users compilation
IEnumerable<SyntaxTree> syntaxTrees = context.Compilation.SyntaxTrees;
// add the filepath of each tree to the class we're building
foreach (SyntaxTree tree in syntaxTrees)
{
sourceBuilder.AppendLine($@"Console.WriteLine(@"" - {tree.FilePath}"");");
}
// finish creating the source to inject
sourceBuilder.Append(@"
}
}
}");
// inject the created source into the users compilation
context.AddSource("helloWorldGenerated", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
public void Initialize(GeneratorInitializationContext context)
{
}
}
}
從這段程式碼,你應該可以看出這裡的「原始碼產生器」非常的「原始」,其實就是「組字串」成一個 C# 程式碼而已,這是 Source Generator 最簡單的撰寫方式,我想應該大家都可以勝任這個工作。
比較進階的方式則是使用 SyntaxTree 進行深入分析與修改。
使用 Source Generator 自動產生的類別
從上述程式碼可以看出,我們動態產生的 C# 程式碼會有一個 HelloWorldGenerated
命名空間,有一個靜態類別名稱為 HelloWorld
,有個靜態方法 SayHello()
被建立。
-
修改 ConsoleApp1\Program.cs
程式碼如下
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
HelloWorldGenerated.HelloWorld.SayHello();
}
}
}
請注意:目前 Visual Studio Code 的 C# 語言伺服器尚未支援 Source Generators 語法,所以會出現以下錯誤訊息:
-
透過 dotnet run
執行 ConsoleApp1
程式
dotnet run
關於 Visual Studio 2019
目前 Visual Studio 2019 16.8
已經支援 Source Generators 特性,所以無論 IntelliSense 或各種開發輔助都支援的非常到位,你甚至可以看到「自動產生」的程式碼內容!
-
建立 *.sln
方案檔,並加入專案到方案中
dotnet new sln
dotnet sln add MySourceGenerator
dotnet sln add ConsoleApp1
-
使用 Visual Studio 2019 開啟專案並執行
如果你在 Visual Studio 2019 看到以下錯誤,千萬不要覺得意外,目前就算是最新的 Visual Studio 2019 16.8 版,目前對 Source Generators 還是時好時壞,非常不穩定!
請手動將 MySourceGenerator.csproj
專案檔內的 <TargetFramework>
修改設定為 netstandard2.0
,你的 Visual Studio 2019 就可以認得 Source Generators 產生的類別了!
如果你按 F12
移至定義,就會看到這些自動產生的程式碼內容:
注意:Visual Studio 2019 對於 Source Generators 的支援度還很有限,問題很多,主要是會經常出現 IntelliSense 消失與編譯錯誤的問題,但即便 Visual Studio 2019 說編譯錯誤,但其實是可以正常編譯與執行的!請追蹤 Source Generators: design-time completion/intellisense is never fixed #44093 這個 Issue 的後續更新!
如何偵錯 Source Generators 自動產生程式碼的過程
你只要加入以下這行程式碼在 MySourceGenerator.cs
的 Initialize(GeneratorInitializationContext context)
方法中即可:
public void Initialize(GeneratorInitializationContext context)
{
System.Diagnostics.Debugger.Launch();
}
當你的 ConsoleApp1
專案有任何更新時,專案只要重新建置,就會自動觸發 Source Generator 執行,畫面上便會自動提示要不要啟動偵錯器:
結語
透過上述簡單的實作,你應該可以看出 Source Generators 的強大魅力,我已經使用 Source Generators 在自己的專案上,成效卓越! 👍
不過我真的也遇到 Source Generators 的許多地雷,耗費了我不少時間:
- 變更 Source Generator 專案中的類別,並不會導致 Generator 自動重新建置!
- 你必須變更主要專案(
ConsoleApp1
)的原始碼,或是清空主要專案,才會讓 Source Generator 重新執行!
- 如果主要專案建置失敗,那麼每次建置都會重新執行 Source Generator
- 你的 Visual Studio 2019 會一直在背景重新建置專案,此時你在編輯程式碼打字的過程中,就會導致 Generator 類別不斷重複執行!
- 從 Visual Studio 2019 的輸出視窗可以很輕易的看出 Source Generator 的執行錯誤!
- 我有寫一個 Source Generator 範例程式,包含完整的開發步驟與流程:https://github.com/doggy8088/SourceGeneratorDemo
相關連結