Geradores de Origem

Este artigo fornece uma descrição geral dos Geradores de Origem que são fornecidos como parte do SDK da Plataforma de Compilador .NET ("Roslyn"). Os Geradores de Origem permitem que os programadores de C# inspecionem o código do utilizador à medida que estão a ser compilados. O gerador pode criar novos ficheiros de origem C# que são adicionados à compilação do utilizador. Desta forma, tem código que é executado durante a compilação. Inspeciona o programa para produzir ficheiros de origem adicionais que são compilados em conjunto com o resto do código.

Um Gerador de Origem é um novo tipo de componente que os programadores de C# podem escrever que lhe permite fazer duas coisas importantes:

  1. Obtenha um objeto de compilação que represente todo o código de utilizador que está a ser compilado. Este objeto pode ser inspecionado e pode escrever código que funcione com a sintaxe e os modelos semânticos para o código que está a ser compilado, tal como acontece com os analisadores atualmente.

  2. Gerar ficheiros de origem C# que podem ser adicionados a um objeto de compilação durante a compilação. Por outras palavras, pode fornecer código fonte adicional como entrada para uma compilação enquanto o código está a ser compilado.

Quando combinados, estas duas coisas são o que tornam os Geradores de Origem tão úteis. Pode inspecionar o código do utilizador com todos os metadados avançados que o compilador compila durante a compilação. Em seguida, o gerador emite código C# novamente na mesma compilação baseada nos dados que analisou. Se estiver familiarizado com os Analisadores Roslyn, pode considerar os Geradores de Origem como analisadores que podem emitir código fonte C#.

Os geradores de origem são executados como uma fase de compilação visualizada abaixo:

Gráfico que descreve as diferentes partes da geração de origem

Um Gerador de Origem é uma assemblagem .NET Standard 2.0 que é carregada pelo compilador juntamente com quaisquer analisadores. É utilizável em ambientes onde os componentes .NET Standard podem ser carregados e executados.

Importante

Atualmente, apenas as assemblagens .NET Standard 2.0 podem ser utilizadas como Geradores de Origem.

Cenários comuns

Existem três abordagens gerais para inspecionar o código do utilizador e gerar informações ou código com base nessa análise utilizada atualmente pelas tecnologias:

  • Reflexo de tempo de execução.
  • Fazer malabarismos com tarefas MSBuild.
  • Tecelagem de Idioma Intermédio (IL) (não abordado neste artigo).

Os Geradores de Origem podem ser uma melhoria em relação a cada abordagem.

Reflexo do runtime

A reflexão runtime é uma tecnologia poderosa que foi adicionada ao .NET há muito tempo. Existem inúmeros cenários para utilizá-lo. Um cenário comum é efetuar alguma análise do código de utilizador quando uma aplicação é iniciada e utilizar esses dados para gerar itens.

Por exemplo, ASP.NET Core utiliza reflexão quando o seu serviço Web é executado pela primeira vez para detetar construções que definiu para que possa "ligar" coisas como controladores e páginas do Razor. Embora isto lhe permita escrever código simples com abstrações poderosas, vem com uma penalização de desempenho no tempo de execução: quando o seu serviço Web ou aplicação é iniciado pela primeira vez, não pode aceitar quaisquer pedidos até que todo o código de reflexão do runtime que detete informações sobre o seu código esteja concluído em execução. Embora esta penalização de desempenho não seja enorme, é um custo fixo que não pode melhorar na sua própria aplicação.

Com um Gerador de Origem, a fase de deteção do controlador do arranque pode acontecer no momento da compilação. Um gerador pode analisar o código fonte e emitir o código de que precisa para "ligar" a sua aplicação. A utilização de geradores de origem pode resultar em tempos de arranque mais rápidos, uma vez que uma ação que ocorre atualmente no tempo de execução pode ser enviada para o tempo de compilação.

Fazer malabarismos com tarefas MSBuild

Os Geradores de Origem podem melhorar o desempenho de formas que não se limitam à reflexão no tempo de execução para detetar também tipos. Alguns cenários envolvem chamar a tarefa MSBuild C# (chamada CSC) várias vezes para que possam inspecionar os dados de uma compilação. Como pode imaginar, chamar o compilador mais do que uma vez afeta o tempo total necessário para criar a sua aplicação. Estamos a investigar como os Geradores de Origem podem ser utilizados para evitar a necessidade de fazer malabarismos com tarefas msBuild como esta, uma vez que os geradores de origem não oferecem apenas alguns benefícios de desempenho, mas também permitem que as ferramentas funcionem no nível certo de abstração.

Outra capacidade que os Geradores de Origem podem oferecer é a eliminação da utilização de algumas APIs "escritas com cadeias", como a forma como ASP.NET Core o encaminhamento entre controladores e páginas do Razor funcionam. Com um Gerador de Origem, o encaminhamento pode ser fortemente escrito com as cadeias necessárias a serem geradas como um detalhe de tempo de compilação. Isto reduziria o número de vezes que um literal de cadeia de carateres mal escrito leva a que um pedido não atingisse o controlador correto.

Introdução aos geradores de origem

Neste guia, irá explorar a criação de um gerador de origem com a ISourceGenerator API.

  1. Crie uma aplicação de consola .NET. Este exemplo utiliza .NET 6.

  2. Substitua a Program classe pelo seguinte código. O código seguinte não utiliza instruções de nível superior. O formulário clássico é necessário porque este primeiro gerador de origem escreve um método parcial nessa Program classe:

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

    Nota

    Pode executar este exemplo tal como está, mas ainda nada acontecerá.

  3. Em seguida, vamos criar um projeto gerador de origem que irá implementar a contraparte do partial void HelloFrom método.

  4. Crie um projeto de biblioteca padrão .NET destinado ao netstandard2.0 moniker de arquitetura de destino (TFM). Adicione os pacotes NuGet Microsoft.CodeAnalysis.Analyzers e 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>
    

    Dica

    O projeto do gerador de origem tem de visar o netstandard2.0 TFM, caso contrário, não funcionará.

  5. Crie um novo ficheiro C# com o nome HelloSourceGenerator.cs que especifique o seu próprio Gerador de Origem da seguinte forma:

    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
            }
        }
    }
    

    Um gerador de origem tem de implementar a Microsoft.CodeAnalysis.ISourceGenerator interface e ter o Microsoft.CodeAnalysis.GeneratorAttribute. Nem todos os geradores de origem necessitam de inicialização, o que acontece com esta implementação de exemplo, onde ISourceGenerator.Initialize está vazio.

  6. Substitua o conteúdo do ISourceGenerator.Execute método pela seguinte implementação:

    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
            }
        }
    }
    

    A partir do context objeto, podemos aceder ao ponto de entrada ou Main método das compilações. A mainMethod instância é um IMethodSymbol, e representa um método ou símbolo semelhante a um método (incluindo construtor, destrutor, operador ou acessório de propriedade/evento). O Microsoft.CodeAnalysis.Compilation.GetEntryPoint método devolve o IMethodSymbol para o ponto de entrada do programa. Outros métodos permitem-lhe encontrar qualquer símbolo de método num projeto. A partir deste objeto, podemos raciocinar sobre o espaço de nomes que contém (se existir um) e o tipo. Neste source exemplo, encontra-se uma cadeia interpolada que cria o código fonte a ser gerado, onde os buracos interpolados são preenchidos com o espaço de nomes e as informações do tipo. O source é adicionado ao context com um nome de sugestão. Para este exemplo, o gerador cria um novo ficheiro de origem gerado que contém uma implementação do partial método na aplicação de consola. Pode escrever geradores de origem para adicionar qualquer origem que pretender.

    Dica

    O hintName parâmetro do GeneratorExecutionContext.AddSource método pode ser qualquer nome exclusivo. É comum fornecer uma extensão de ficheiro C# explícita, como ".g.cs" ou ".generated.cs" para o nome. O nome do ficheiro ajuda a identificar o ficheiro como sendo gerado pela origem.

  7. Temos agora um gerador funcional, mas precisamos de o ligar à nossa aplicação de consola. Edite o projeto de aplicação de consola original e adicione o seguinte, substituindo o caminho do projeto pelo do projeto .NET Standard que criou acima:

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

    Esta nova referência não é uma referência de projeto tradicional e tem de ser editada manualmente para incluir os OutputItemType atributos e ReferenceOutputAssembly . Para obter mais informações sobre os OutputItemType atributos e ReferenceOutputAssembly do , veja Common MSBuild project items: ProjectReference (Itens comuns do projeto MSBuild: ProjectReference).ProjectReference

  8. Agora, quando executar a aplicação de consola, deverá ver que o código gerado é executado e impresso no ecrã. A aplicação de consola em si não implementa o método, em vez disso, é a origem HelloFrom gerada durante a compilação do projeto Gerador de Origem. O texto seguinte é um exemplo de saída da aplicação:

    Generator says: Hi from 'Generated Code'
    

    Nota

    Poderá ter de reiniciar o Visual Studio para ver o IntelliSense e eliminar erros, uma vez que a experiência de ferramentas está a ser ativamente melhorada.

  9. Se estiver a utilizar o Visual Studio, pode ver os ficheiros gerados pela origem. Na janela Explorador de Soluções , expanda o ficheiro Dependencies>Analyzers>SourceGenerator>SourceGenerator.HelloSourceGenerator e faça duplo clique no ficheiro Program.g.cs .

    Visual Studio: ficheiros gerados pela origem do Explorador de Soluções.

    Quando abrir este ficheiro gerado, o Visual Studio indicará que o ficheiro é gerado automaticamente e que não pode ser editado.

    Visual Studio: ficheiro Program.g.cs gerado automaticamente.

  10. Também pode definir propriedades de compilação para guardar o ficheiro gerado e controlar onde os ficheiros gerados estão armazenados. No ficheiro de projeto da aplicação de consola, adicione o elemento a um <PropertyGroup>e defina o <EmitCompilerGeneratedFiles> respetivo valor como true. Crie o seu projeto novamente. Agora, os ficheiros gerados são criados em obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator. Os componentes do mapa de caminho para a configuração da compilação, a arquitetura de destino, o nome do projeto do gerador de origem e o nome do tipo completamente qualificado do gerador. Pode escolher uma pasta de saída mais conveniente ao adicionar o <CompilerGeneratedFilesOutputPath> elemento ao ficheiro de projeto da aplicação.

Passos seguintes

O Source Generators Cookbook aborda alguns destes exemplos com algumas abordagens recomendadas para resolvê-los. Além disso, temos um conjunto de exemplos disponíveis no GitHub que pode experimentar sozinho.

Pode saber mais sobre os Geradores de Origem nestes artigos: