﻿# 新手上路 C# 原始碼產生器 (Source Generators)

最近迷上 C# 原始碼產生器 (Source Generators) 這門相當新穎的技術，跟以往常用的 T4 (Text Template Transformation Toolkit) 產生器技術不太一樣，這個 Source Generators 是屬於 Roslyn 編譯器的技術之一，讓你在專案建置的過程中，可以對**正在編譯**的 C# 原始碼進行**增補**，動態加入「額外」的原始碼，最後再編譯在一起，非常神奇又實用的技術，讓人非常有想像空間！ 👍

### 區別 T4 與 Source Generators 的差異

從概念上來說，這兩個「程式碼產生器」技術相當類似，不過執行方式卻完全不同：

1.  [T4](https://learn.microsoft.com/zh-tw/visualstudio/modeling/code-generation-and-t4-text-templates?view=vs-2019&WT.mc_id=DT-MVP-4015686)
    
    主要是內嵌在 Visual Studio 中的一門技術，打從 Visual Studio 2005 開始就存在，不過這套產品一直不受微軟愛戴，直到 Visual Studio 2019 都還缺乏很基本的語法高量功能，都要靠擴充套件才能顯示語法高量，而且還很容易有 Bug 出現，尤其是使用「深色」模式的布景主題。使用 T4 技術來產生任意程式碼有個好處，他可以讓你在 Visual Studio 開發工具裡自動產生任意以「文字」為主的檔案內容，可以是 C#、可以是 VB、也可以是 XML 檔案，可以說是非常靈活。
    
2.  [Source Generators](https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/?WT.mc_id=DT-MVP-4015686)
    
    主要執行在 Roslyn 編譯器的管道中 (compiler pipeline)，所以產生器**只能**執行在 C# 編譯時期，你可以透過 [SyntaxTree](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.syntaxtree?view=roslyn-dotnet&WT.mc_id=DT-MVP-4015686) 讀取與分析目前專案中所有 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 開發！

1.  建立一個 Console 應用程式 (TFM: `net5.0`)
    
    ```bash
    dotnet new console -n ConsoleApp1
    ```
    
2.  建立一個 Class library 類別庫專案 (TFM: `netstandard2.0`)
    
    ```bash
    dotnet new classlib -n MySourceGenerator
    ```
    
3.  使用 Visual Studio Code 開啟 ConsoleApp1 並加入 MySourceGenerator 資料夾到工作區
    
    ![使用 Visual Studio Code 開啟 ConsoleApp1 並加入 MySourceGenerator 資料夾到工作區](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/585922e96bd5979d9a8a-100613036-da6d6480-334e-11eb-91e3-f6a5aaf47981.png)
    
    加入成功後的畫面如下：
    
    ![工作區 (Workspace)](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/2e384a8ca8ad7687edc0-100613226-27513b00-334f-11eb-8ea0-1a727ba8e25d.png)
    
    建議可以將多專案的工作區儲存起來，方便下次開啟：
    
    ![將多專案的工作區儲存起來](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/eb223306efdaee55db4a-100613540-acd4eb00-334f-11eb-89c2-acbf1d330cb1.png)
    
4.  將 MySourceGenerator 專案加入成為 `ConsoleApp1` 的專案參考 (Project Reference)
    
    ```bash
    dotnet add reference ..\MySourceGenerator\MySourceGenerator.csproj
    ```
    
    事實上這個命令會在 `ConsoleApp1\ConsoleApp1.csproj` 加入以下 `<ItemGroup>` 定義：
    
    ```xml
    <ItemGroup>
      <ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj" />
    </ItemGroup>
    ```
    
    不過，這個設定必須進行微調，請調整成以下設定，這才能真正成為一個有效的 Source Generator 專案參考：
    
    ```xml
    <ItemGroup>
      <ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj"
                        OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
    </ItemGroup>
    ```
    
    > 這裡的 `OutputItemType="Analyzer"` 用來設定 `MySourceGenerator` 專案是一個「分析器」專案。而 `ReferenceOutputAssembly="false"` 則是設定該專案不會成為該專案的參考組件。詳見 [Common MSBuild project items](https://learn.microsoft.com/en-us/visualstudio/msbuild/common-msbuild-project-items?view=vs-2019&WT.mc_id=DT-MVP-4015686) 文件。
    

上述這四個步驟，是相當重要的初步設定，尤其是最後一步，沒有正確設定的話 Source Generators 是完全無法執行的！

### 撰寫基礎 Source Generator 類別

以下步驟都是在 `MySourceGenerator` 專案下進行設定：

1.  請將 `Class1.cs` 更名為 `MySourceGenerator.cs`，並將內容修改為以下內容：
    
    ```csharp
    using System;
    
    namespace MySourceGenerator
    {
        public class MySourceGenerator
        {
        }
    }
    ```
    
2.  加入 [Microsoft.CodeAnalysis.CSharp](https://www.nuget.org/packages/Microsoft.CodeAnalysis.CSharp) 與 [Microsoft.CodeAnalysis.Analyzers](https://www.nuget.org/packages/Microsoft.CodeAnalysis.Analyzers) 套件
    
    ```bash
    dotnet add package Microsoft.CodeAnalysis.CSharp
    dotnet add package Microsoft.CodeAnalysis.Analyzers
    ```
    
3.  替 `MySourceGenerator` 類別套用 `[Generator]` Attribute 與實作 ISourceGenerator 介面
    
    ```csharp
    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()` 方法！
    
4.  加入「程式碼產生器」到 `Execute()` 方法中 (記得加入必要的 `using` 匯入命名空間)
    
    我直接提供官方提供的 Source Generators 範例程式來進行實作，請參考 [HelloWorldGenerator.cs](https://github.com/dotnet/roslyn-sdk/blob/08225dfd2206d29c1190edec2ed7777a83f29e2b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.cs) 檔案：
    
    ```csharp
    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](https://learn.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.syntaxtree?view=roslyn-dotnet&WT.mc_id=DT-MVP-4015686) 進行深入分析與修改。
    

### 使用 Source Generator 自動產生的類別

從上述程式碼可以看出，我們動態產生的 C# 程式碼會有一個 `HelloWorldGenerated` 命名空間，有一個靜態類別名稱為 `HelloWorld`，有個靜態方法 `SayHello()` 被建立。

1.  修改 `ConsoleApp1\Program.cs` 程式碼如下
    
    ```csharp
    using System;
    
    namespace ConsoleApp1
    {
        class Program
        {
            static void Main(string[] args)
            {
                HelloWorldGenerated.HelloWorld.SayHello();
            }
        }
    }
    ```
    
    請注意：目前 Visual Studio Code 的 C# 語言伺服器尚未支援 Source Generators 語法，所以會出現以下錯誤訊息：
    
    ![請注意：目前 Visual Studio Code 的 C# 語言伺服器尚未支援 Source Generators 語法，所以會出現以下錯誤訊息：](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/1b97575fd64828163648-100617650-c4af6d80-3355-11eb-96a8-dbe49cd34296.png)
    
2.  透過 `dotnet run` 執行 `ConsoleApp1` 程式
    
    ```bash
    dotnet run
    ```
    
    ![透過 dotnet run 執行 ConsoleApp1 程式](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/efc2278f5ff8b6a04ca4-100617860-04765500-3356-11eb-9a44-f1e52af5b29e.png)
    

### 關於 Visual Studio 2019

目前 Visual Studio 2019 `16.8` 已經支援 Source Generators 特性，所以無論 IntelliSense 或各種開發輔助都支援的非常到位，你甚至可以看到「自動產生」的程式碼內容！

1.  建立 `*.sln` 方案檔，並加入專案到方案中
    
    ```bash
    dotnet new sln
    dotnet sln add MySourceGenerator
    dotnet sln add ConsoleApp1
    ```
    
2.  使用 Visual Studio 2019 開啟專案並執行
    
    如果你在 Visual Studio 2019 看到以下錯誤，千萬不要覺得意外，目前就算是最新的 Visual Studio 2019 16.8 版，目前對 Source Generators 還是時好時壞，非常不穩定！
    
    ![如果你在 Visual Studio 2019 看到以下錯誤，千萬不要覺得意外，目前就算是最新的 Visual Studio 2019 16.8 版，目前對 Source Generators 還是時好時壞，非常不穩定！](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/616b1b3c8a19f6cab5e0-100619128-aa768f00-3357-11eb-8693-edd519209637.png)
    
    請手動將 `MySourceGenerator.csproj` 專案檔內的 `<TargetFramework>` 修改設定為 `netstandard2.0`，你的 Visual Studio 2019 就可以認得 Source Generators 產生的類別了！
    
    ![Visual Studio 2019 可以認得 Source Generators 產生的類別](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/5011146db8b121c64862-100621307-7bade800-335a-11eb-9776-81a30108d752.png)
    
    如果你按 `F12` 移至定義，就會看到這些自動產生的程式碼內容：
    
    ![This file is auto-generated by the generator 'MySourceGenerator.MySourceGenerator' and cannot be edited.](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/27f5e57a7c5d7169bf72-100621471-af890d80-335a-11eb-8819-512f3f010eda.png)
    

> 注意：Visual Studio 2019 對於 Source Generators 的支援度還很有限，問題很多，主要是會經常出現 IntelliSense 消失與編譯錯誤的問題，但即便 Visual Studio 2019 說編譯錯誤，但其實是可以正常編譯與執行的！請追蹤 [Source Generators: design-time completion/intellisense is never fixed #44093](https://github.com/dotnet/roslyn/issues/44093) 這個 Issue 的後續更新！

### 如何偵錯 Source Generators 自動產生程式碼的過程

你只要加入以下這行程式碼在 `MySourceGenerator.cs` 的 `Initialize(GeneratorInitializationContext context)` 方法中即可：

```bash
public void Initialize(GeneratorInitializationContext context)
{
    System.Diagnostics.Debugger.Launch();
}
```

當你的 `ConsoleApp1` 專案有任何更新時，專案只要重新建置，就會自動觸發 Source Generator 執行，畫面上便會自動提示要不要啟動偵錯器：

![Choose Just-In-Time Debugger](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/90108484f21baabf8e7a-100622229-946acd80-335b-11eb-9d51-2a50982fda88.png)

### 結語

透過上述簡單的實作，你應該可以看出 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](https://github.com/doggy8088/SourceGeneratorDemo)

### 相關連結

-   Introducing C# Source Generators | .NET Blog
-   [New C# Source Generator Samples | .NET Blog](https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/?WT.mc_id=DT-MVP-4015686)
-   [Roslyn - Source Generators](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md)
-   [Roslyn - Source Generators Cookbook](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md)
-   [How to generate code using Roslyn source generators in real world scenarios](https://www.cazzulino.com/source-generators.html)
-   [討論 Source Generator 與 T4 的關係](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/?WT.mc_id=DT-MVP-4015686#comment-5589)
-   [Source Generators in C# | Visual Studio Toolbox | Channel 9](https://channel9.msdn.com/Shows/Visual-Studio-Toolbox/Source-Generators-in-CSharp?WT.mc_id=DT-MVP-4015686)
-   超硬核 Roslyn Source Generators 原始碼導覽
    -   [Episode 21: Roslyn Source Generators - YouTube](https://www.youtube.com/watch?v=JSFZ3qDx83g&feature=emb_logo)
-   看完這些範例就都會寫了！
    -   [https://github.com/dotnet/roslyn-sdk/tree/08225dfd2206d29c1190edec2ed7777a83f29e2b/samples/CSharp/SourceGenerators](https://github.com/dotnet/roslyn-sdk/tree/08225dfd2206d29c1190edec2ed7777a83f29e2b/samples/CSharp/SourceGenerators)
-   [Source Generator Playground - @davidwengier](https://github.com/davidwengier/SourceGeneratorPlayground)
