最近迷上 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
相關連結