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

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

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

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

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

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

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

Диаграмма, описывающая различные этапы формирования исходного кода

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

Важно!

Сейчас в качестве генераторов исходного кода можно использовать только сборки .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 Standard, предназначенный для моникера целевой 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.9.2" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
      </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 . Дополнительные сведения об атрибутах ProjectReferenceи ReferenceOutputAssembly см. в OutputItemType разделе Общие элементы проекта MSBuild: ProjectReference.

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

    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, на котором вы можете попрактиковаться.

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