Генераторы источников

Эта статья содержит обзор генераторов исходного кода, входящих в состав пакета SDK для .NET Compiler Platform ("Roslyn"). Генераторы источников позволяют разработчикам C# проверять пользовательский код по мере компиляции. Генератор может создавать новые C# исходные файлы во время компиляции пользователя. Таким образом, у вас есть код, который выполняется во время компиляции. Он проверяет программу, чтобы создать дополнительные исходные файлы, скомпилированные вместе с остальными кодами.

Генератор исходного кода — это новый тип компонента, который разработчики C# могут использовать, чтобы выполнять два основных действия:

  1. Извлеките объект компиляции, представляющий весь компилируемый пользовательский код. Этот объект можно проверить, а вы можете написать код, который работает с синтаксисом и семантическими моделями компилируемого кода, как и с анализаторами, уже сегодня.

  2. Создайте C# исходные файлы, которые можно добавить в объект компиляции во время компиляции. Иными словами, во время компиляции кода можно указать дополнительный исходный код в качестве входных данных для компиляции.

В сочетании эти две вещи делают также полезным генераторы исходного кода. Вы можете проверить пользовательский код со всеми расширенными метаданными, которые компилятор создает во время компиляции. Затем генератор выдает C# код обратно в ту же компиляцию, которая основана на проанализированных данных. Если вы знакомы с анализаторами Roslyn, вы можете рассматривать исходные генераторы как анализаторы, которые могут выдавать исходный код C#.

Генераторы исходного кода выполняются в фазе компиляции, представленной ниже.

Graphic describing the different parts of source generation

Генератор исходного кода — это сборка .NET Standard 2.0, которая загружается компилятором вместе с любыми анализаторами. Она пригодна для использования в средах, где можно загрузить и запустить стандартные компоненты .NET.

Важно!

Сейчас в качестве генераторов исходного кода можно использовать только сборки .NET Standard 2.0.

Распространенные сценарии

В современных технологиях используется три общих подхода к проверке пользовательского кода и созданию информации или кода на основе этого анализа:

  • отражение среды выполнения;
  • жонглирование задачами MSBuild;
  • применение промежуточного языка (не рассматривается в этой статье).

Генераторы исходного кода могут быть усовершенствованы при любом подходе.

Отражение среды выполнения

Отражение среды выполнения — это мощная технология, которая была добавлена в .NET довольно давно. Существует множество сценариев для ее использования. Распространенный сценарий — выполнение некоторого анализа пользовательского кода при запуске приложения и использовании этих данных для создания вещей.

Например, ASP.NET Core использует отражение при первом запуске веб-службы для обнаружения определенных конструкций, чтобы "подсоединить" такие вещи, как контроллеры и Razor Pages. Хотя это позволяет писать простой код с мощными абстракциями, он поставляется с штрафом производительности во время выполнения: когда веб-служба или приложение впервые запускается, он не может принимать какие-либо запросы, пока не будет завершен весь код отражения среды выполнения, который обнаруживает сведения о коде. Хотя эта производительность не огромна, это несколько фиксированных затрат, которые вы не можете улучшить в вашем собственном приложении.

При использовании генератора источников этап обнаружения контроллера может происходить во время компиляции. Генератор может анализировать исходный код и выдавать код, необходимый для подключения приложения. Использование генераторов источников может привести к более быстрому запуску, так как действие, выполняемое во время выполнения, может быть отправлено во время компиляции.

Жонглирование задачами MSBuild

Генераторы исходного кода могут повысить производительность так, чтобы не ограничиваться отражением во время выполнения для обнаружения типов. Некоторые сценарии предусматривают многократный вызов задачи C# MSBuild (называемой CSC), чтобы они могли проверять данные из компиляции. Как вы можете себе представить, многократный вызов компилятора влияет на общее время, затрачиваемое на создание приложения. Мы изучаем, как можно использовать генераторы исходного кода, чтобы избежать задач, таких как эта, так как генераторы исходного кода не просто предлагают некоторые преимущества MSBuild для повышения производительности, но также позволяют инструментам работать на нужном уровне абстракции.

Кроме того, генераторы источников возможностей могут использовать некоторые "строковые типизированные" API, например, как ASP.NET Core маршрутизацию между контроллерами и страницами razor. При использовании генератора исходного кода маршрутизация может быть строго типизирована с помощью необходимых строк, формируемых как данные времени компиляции. Это позволит уменьшить количество неправильно введенных строковых литералом, что приводит к тому, что запрос не попадает в правильный контроллер.

Знакомство с генераторами исходного кода

В этом разделе вы ознакомитесь с созданием генератора исходного кода с помощью API ISourceGenerator.

  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. Создайте проект стандартной библиотеки .NET, предназначенный для моникера целевой netstandard2.0 платформы (TFM). Добавьте пакеты NuGet Microsoft.CodeAnalysis.Analyzers и Microsoft.CodeAnalysis.CSharp:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
          <PrivateAssets>all</PrivateAssets>
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
      </ItemGroup>
    
    </Project>
    

    Совет

    Проект генератора исходного кода должен быть нацелен на TFM netstandard2.0, в противном случае он не будет работать.

  5. Создайте новый файл C# с именем HelloSourceGenerator.cs, в котором указан ваш генератор исходного кода, например так:

    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 метода в консольном приложении. Вы можете написать генераторы источников, чтобы добавить любой нужный источник.

    Совет

    Значением параметра hintName из метода GeneratorExecutionContext.AddSource может быть любое уникальное имя. Обычно в качестве имени указывается явное расширение файла 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>
    

    Эта новая ссылка не является традиционной ссылкой на проект и должна быть изменена вручную для включения OutputItemType атрибутов и ReferenceOutputAssembly атрибутов. Дополнительные сведения об OutputItemType и ReferenceOutputAssembly атрибутах ProjectReferenceсм. в разделе "Общие MSBuild элементов проекта: ProjectReference".

  8. Теперь при запуске консольного приложения созданный код должен выполнятся с выводом соответствующего процесса на экран. Консольное приложение не реализует HelloFrom метод, а источник, созданный во время компиляции из проекта генератора исходного кода. Следующий текст является примером выходных данных приложения:

    Generator says: Hi from 'Generated Code'
    

    Примечание

    Возможно, вам придется перезапустить Visual Studio, чтобы просмотреть данные IntelliSense и исправить ошибки, так как инструментарий активно совершенствуется.

  9. Если вы используете Visual Studio, вы можете увидеть файлы, созданные в исходном коде. В окне Обозреватель решений развернитеанализаторы>зависимостей>SourceGenerator SourceGenerator.HelloSourceGenerator> и дважды щелкните файл Program.g.cs.

    Visual Studio: Solution Explorer source generated files.

    При открытии созданного файла Visual Studio будет указывать, что файл создается автоматически и что его невозможно изменить.

    Visual Studio: Auto-generated Program.g.cs file.

  10. Вы также можете задать свойства сборки, чтобы сохранить созданный файл и управлять местом хранения созданных файлов. В файле проекта консольного приложения добавьте <EmitCompilerGeneratedFiles> элемент в <PropertyGroup>элемент и задайте для нее значение true. Повторите сборку проекта. Теперь созданные файлы создаются в obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator. Компоненты пути сопоставляют конфигурацию сборки, целевую платформу, имя проекта генератора источников и полное имя типа генератора. Вы можете выбрать более удобную выходную папку, добавив <CompilerGeneratedFilesOutputPath> элемент в файл проекта приложения.

Дальнейшие действия

Сборник рецептов для генераторов исходного кода содержит примеры и некоторые рекомендуемые подходов по их решению. Кроме того, у нас есть набор примеров на портале GitHub, на котором вы можете попрактиковаться.

Дополнительные сведения о генераторах источников см. в следующих статьях: