來源產生器

本文提供來源產生器的概觀,這些來源產生器隨附于.NET Compiler Platform (「Roslyn」) SDK。 來源產生器可讓 C# 開發人員檢查正在編譯的使用者程式碼。 產生器可即時建立新的 C# 來源檔案,這些檔案會新增至使用者的編譯。 如此一來,您便擁有會在編譯期間執行的程式碼。 這會檢查程式,以產生與其他程式碼一起編譯的其他來源檔案。

來源產生器是 C# 開發人員可以撰寫的新元件,可讓您執行兩個主要動作:

  1. 擷取編譯物件,該物件代表正在編譯的所有使用者程式碼。 您可以檢查這個物件,並可撰寫程式碼,適用於正在編譯程式碼的語法和語意模型,一如現有的流量分析器。

  2. 在編譯期間產生可新增至編譯物件的 C# 來源檔案。 換句話說,您可以在編譯代碼時提供額外的原始程式碼於編譯時輸入。

合併時,這兩件事會讓來源產生器變得很有用。 您可以使用編譯器在編譯期間建置的所有豐富中繼資料來檢查使用者程式碼。 然後,您的產生器會發出 C# 程式碼,並傳回基於您分析資料的相同編譯中。 如果您熟悉 Roslyn 分析器,您可以將來源產生器視為可發出 C# 原始程式碼的分析器。

來源產生器會以視覺化方式執行下列編譯階段:

描述來源產生器不同部分的圖表

來源產生器是編譯器所載入的 .NET Standard 2.0 元件,以及任何分析器。 它可以在可載入和執行 .NET Standard 元件的環境中使用。

重要

目前只能使用 .NET Standard 2.0 元件作為來源產生器。

常見的案例

目前有三種一般方法來檢查使用者程式碼,並根據技術所使用的分析來產生資訊或程式碼:

  • 執行階段反映。
  • 混用 MSBuild 工作。
  • 中繼語言 (IL) 編排 (未在本文討論)。

來源產生器可用來改善每個方法。

執行階段反映

執行時間反映是一項功能強大的技術,已于一段時間前新增至 .NET。 有許多案例利用這項技術。 常見的案例是在應用程式啟動時執行使用者程式碼的一些分析,並使用該資料來產生項目。

例如,當您的 Web 服務第一次執行時,ASP.NET Core使用反映來探索您已定義的建構,讓它可以「連接」控制器和 razor 頁面等專案。 雖然這可讓您撰寫具有強大抽象概念的直接程式碼,但在執行階段會產生效能負面影響:當您第一次啟動 Web 服務或應用程式時,除非探索程式碼相關資訊的所有執行階段反映程式碼都已完成執行,否則無法接受任何要求。 雖然此效能負面影響不大,但這會是您無法在自己的應用程式中自行改善的固定成本。

使用來源產生器時,啟動的控制器探索階段可能會改為在編譯時期發生。 產生器可以分析您的原始程式碼,並發出需要「連線」應用程式的程式碼。 使用來源產生器可能會導致稍微快一些的啟動時間,因為目前執行階段發生的動作可能會推送至編譯時間。

混用 MSBuild 工作

來源產生器也可以透過不限於執行階段反映的方式來改善效能,以探索類型。 某些案例牽涉到多次呼叫 MSBuild C# 工作 (稱為 CSC),以便檢查編譯中的資料。 如您所想,多次呼叫編譯器會影響建置應用程式所需的總時間。 我們正在調查來源產生器如何用來避免混用 MSBuild 工作的需求,因為來源產生器不只提能供一些效能優勢,也允許工具以正確的抽象層級運作。

來源產生器可以提供的另一項功能是略過某些「字串類型」API 的使用,例如控制器與 razor 頁面之間 ASP.NET Core路由的運作方式。 使用來源產生器時,路由能搭配產生作為編譯時間詳細資料的必要字串成為強型別。 這可減少錯誤字串常值導致要求未達到正確控制器的次數。

開始使用來源產生器

在本指南中,您將探索如何使用 ISourceGenerator API 建立來源產生器。

  1. 建立 .NET 主控台應用程式。 此範例使用 .NET 6。

  2. 使用下列程式碼取代 Program 類別。 下列程式碼不會使用最上層陳述式。 傳統形式是必要的,因為第一個來源產生器會在該 Program 類別中寫入部分方法:

    namespace ConsoleApp;
    
    partial class Program
    {
        static void Main(string[] args)
        {
            HelloFrom("Generated Code");
        }
    
        static partial void HelloFrom(string name);
    }
    

    注意

    您可以依原樣執行此樣本,但目前不會發生任何動作。

  3. 接下來,我們將建立來源產生器專案,以實作 partial void HelloFrom 方法對應項目。

  4. 建立以目標 Framework Moniker (TFM) 為目標的 netstandard2.0 .NET 標準程式庫專案。 新增 NuGet 套件 Microsoft.CodeAnalysis.AnalyzersMicrosoft.CodeAnalysis.CSharp

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
      </ItemGroup>
    
    </Project>
    

    提示

    來源產生器專案需要以 netstandard2.0 TFM 為目標,否則將無法運作。

  5. 建立名為 HelloSourceGenerator.cs 的新 C# 檔案,指定您自己的來源產生器,如下所示:

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Code generation goes here
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    來源產生器必須同時實作 Microsoft.CodeAnalysis.ISourceGenerator 介面,而且具有 Microsoft.CodeAnalysis.GeneratorAttribute。 並非所有來源產生器都需要初始化,這是 ISourceGenerator.Initialize 是空白的範例實作情況。

  6. 以下列實作取代 ISourceGenerator.Execute 方法的內容:

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Find the main method
                var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);
    
                // Build up the source code
                string source = $@"// <auto-generated/>
    using System;
    
    namespace {mainMethod.ContainingNamespace.ToDisplayString()}
    {{
        public static partial class {mainMethod.ContainingType.Name}
        {{
            static partial void HelloFrom(string name) =>
                Console.WriteLine($""Generator says: Hi from '{{name}}'"");
        }}
    }}
    ";
                var typeName = mainMethod.ContainingType.Name;
    
                // Add the source code to the compilation
                context.AddSource($"{typeName}.g.cs", source);
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    context 物件中,我們可以存取編譯的進入點或 Main 方法。 mainMethod 執行個體是 IMethodSymbol,代表方法或類似方法的符號 (包括建構函式、解構函式、運算子或屬性/事件存取子)。 方法 Microsoft.CodeAnalysis.Compilation.GetEntryPoint 會傳回程式進入點的 IMethodSymbol。 其他方法可讓您在專案中尋找任何方法符號。 從這個物件我們可以推斷包含命名空間 (如果存在) 和型別。 此範例中的 source 是差補字串,範本要求產生原始程式碼,其中插入的漏洞會填入包含的命名空間和型別資訊。 source 會以提示名稱新增至 context。 在此範例中,產生器會建立新的產生的來源檔案,其中包含主控台應用程式中 partial 方法的實作。 您可以撰寫來源產生器來新增您想要的任何來源。

    提示

    GeneratorExecutionContext.AddSource 方法中的 hintName 參數可以是任何唯一的名稱。 通常提供明確的 C# 副檔名,例如 ".g.cs"".generated.cs" 作為名稱。 檔案名有助於將檔案識別為產生的來源。

  7. 我們現在有正常運作的產生器,但還需要與主控台應用程式連線。 編輯原始主控台應用程式專案並新增下列專案,並將專案路徑取代為您在上面建立的 .NET Standard 專案中的專案:

    <!-- Add this as a new ItemGroup, replacing paths and names appropriately -->
    <ItemGroup>
        <ProjectReference Include="..\PathTo\SourceGenerator.csproj"
                          OutputItemType="Analyzer"
                          ReferenceOutputAssembly="false" />
    </ItemGroup>
    

    這個新的參考並非傳統的專案參考,必須以手動編輯包含 OutputItemTypeReferenceOutputAssembly 屬性。 如需 ProjectReferenceOutputItemTypeReferenceOutputAssembly 屬性的詳細資訊,請參閱一般 MSBuild 專案項目:ProjectReference

  8. 現在,當您執行主控台應用程式時,應該就會看到產生的程式碼會執行並列印到畫面。 主控台應用程式本身不會實作 HelloFrom 方法,而是從來源產生器專案編譯期間產生的來源。 下列文字是應用程式的範例輸出:

    Generator says: Hi from 'Generated Code'
    

    注意

    您可能需要重新開啟 Visual Studio,才能看到 IntelliSense 並移除錯誤,因為目前正在積極改善工具體驗。

  9. 如果您使用 Visual Studio,您可以看到來源產生的檔案。 從 [方案總管] 視窗中,展開[相依性] > [分析器] > [SourceGenerator] > [SourceGenerator.HelloSourceGenerator],然後按兩下 [Program.g.cs] 檔案。

    Visual Studio:方案總管來源產生的檔案。

    當您開啟這個產生的檔案時,Visual Studio 會指出檔案已自動產生,且無法編輯。

    Visual Studio:自動產生的 Program.g.cs 檔案。

  10. 您也可以設定組建屬性,以儲存產生的檔案,並控制產生檔案的儲存位置。 在主控台應用程式的專案檔中,將 <EmitCompilerGeneratedFiles> 項目新增至 <PropertyGroup>,並將其值設定為 true。 再次建置您的專案。 現在,產生的檔案會在 obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator 下建立。 路徑的元件會對應至建置組態、目標架構、來源產生器專案名稱,以及產生器的完整類型名稱。 您可以將 <CompilerGeneratedFilesOutputPath> 項目新增至應用程式的專案檔,以選擇更便利的輸出檔案夾。

下一步

來源產生器操作手冊會探討其中一些範例,以及一些解決這些範例的建議方法。 此外,我們在 GitHub 上有一組可供您自行嘗試的範例。

您可以在下列文章中深入了解來源產生器: