Geradores de origem

Este artigo oferece uma visão geral sobre os Geradores de Origem que são fornecidos como parte do SDK do .NET Compiler Platform ("Roslyn"). Os Geradores de Origem permitem que os desenvolvedores do C# inspecionem o código do usuário enquanto ele está sendo compilado. O gerador pode criar novos arquivos de origem C# em tempo real que são adicionados à compilação do usuário. Dessa forma, você tem um código que é executado durante a compilação. Ele inspeciona seu programa para produzir arquivos de origem adicionais que são compilados junto com o restante do código.

Um Gerador de Origem é um novo tipo de componente que os desenvolvedores do C# podem gravar que permite que você faça duas coisas importantes:

  1. Recupere um objeto de compilação que representa todo o código do usuário que está sendo compilado. Esse objeto pode ser inspecionado e você pode gravar um código que funciona com a sintaxe e modelos semânticos para o código que está sendo compilado, assim como nos analisadores de hoje.

  2. Gere arquivos de origem C# que podem ser adicionados a um objeto de compilação durante a compilação. Em outras palavras, você pode fornecer o código-fonte adicional como entrada para uma compilação enquanto o código está sendo compilado.

Quando combinadas, essas duas coisas são o que tornam os Geradores de Origem tão úteis. Você pode inspecionar o código do usuário com todos os metadados avançados que o compilador acumula durante a compilação. Em seguida, o gerador emite o código C# de volta na mesma compilação baseada nos dados analisados. Se você estiver familiarizado com os Analisadores Roslyn, poderá 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 descrevendo as diferentes partes da geração de origem

Um Gerador de Origem é um assembly .NET Standard 2.0 que é carregado pelo compilador junto com todos os analisadores. Pode ser usado em ambientes em que os componentes do .NET Standard podem ser carregados e executados.

Importante

Atualmente, somente assemblies .NET Standard 2.0 podem ser usados como Geradores de Origem.

Cenários comuns

Há três abordagens gerais para inspecionar o código do usuário e gerar informações ou código com base nessa análise usada por tecnologias atualmente:

  • Reflexão de runtime.
  • Fazendo malabarismo com tarefas do MSBuild.
  • Introdução de IL (Linguagem Intermediária) (não discutida neste artigo).

Os Geradores de Origem podem ser uma melhoria em cada abordagem.

Reflexão de runtime

A reflexão de runtime é uma tecnologia poderosa que foi adicionada ao .NET há muito tempo. Há inúmeros cenários para usá-lo. Um cenário comum é executar algumas análises do código do usuário quando um aplicativo for iniciado e usar esses dados para gerar coisas.

Por exemplo, o ASP.NET Core usa a reflexão quando seu serviço Web é executado pela primeira vez para descobrir construções que você definiu para que ele possa "conectar" coisas como controladores e páginas do Razor. Embora isso permita que você grave um código simples com abstrações poderosas, ele vem com uma penalidade de desempenho no runtime: quando seu serviço Web ou aplicativo é iniciado pela primeira vez, ele não pode aceitar nenhuma solicitação até que todo o código de reflexão de runtime que descubra informações sobre seu código seja concluído em execução. Embora essa penalidade de desempenho não seja enorme, é um custo fixo que você não pode melhorar em seu próprio aplicativo.

Com um Gerador de Origem, a fase de descoberta do controlador da inicialização pode ocorrer no tempo de compilação. Um gerador pode analisar o código-fonte e emitir o código necessário para "conectar" seu aplicativo. O uso de geradores de origem pode resultar em alguns tempos de inicialização mais rápidos, uma vez que uma ação que acontece no tempo de execução hoje pode ser enviada por push para o tempo de compilação.

Fazendo malabarismo com tarefas do MSBuild

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

Outro recurso que os Geradores de Origem podem oferecer é evitar o uso de algumas APIs "tipadas com cadeia de caracteres", como a forma como o roteamento do ASP.NET Core entre controladores e as páginas do Razor funcionam. Com um Gerador de Origem, o roteamento pode ser fortemente tipado com as cadeias de caracteres necessárias sendo geradas como um detalhe de tempo de compilação. Isso reduziria o número de vezes que um literal de cadeia de caracteres digitado incorretamente leva a uma solicitação que não atinge o controlador correto.

Introdução aos geradores de origem

Neste guia, você explorará a criação de um gerador de origem usando a API ISourceGenerator.

  1. Crie um aplicativo de console .NET Core. Este exemplo usa o .NET 6.

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

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

    Observação

    Você pode executar este exemplo no estado em que se encontra, mas nada acontecerá ainda.

  3. Em seguida, criaremos um projeto de gerador de origem que implementará o equivalente do método partial void HelloFrom.

  4. Crie um projeto de biblioteca padrão do .NET direcionado ao TFM (Moniker da Estrutura de Destino) netstandard2.0. 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 precisa ser direcionado ao TFM netstandard2.0, caso contrário, ele não funcionará.

  5. Crie um novo arquivo C# chamado HelloSourceGenerator.cs que especifica seu próprio Gerador de Origem da seguinte maneira:

    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 precisa implementar a interface Microsoft.CodeAnalysis.ISourceGenerator e ter o Microsoft.CodeAnalysis.GeneratorAttribute. Nem todos os geradores de origem exigem inicialização, e esse é o caso dessa implementação de exemplo, onde ISourceGenerator.Initialize está vazio.

  6. Substitua o conteúdo do método ISourceGenerator.Execute pelo código a seguir:

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

    No objeto context, podemos acessar o ponto de entrada das compilações ou o método Main. A instância mainMethod é um IMethodSymbol, e representa um método ou símbolo semelhante a um método (incluindo construtor, destruidor, operador ou acessador de propriedade/evento). O método Microsoft.CodeAnalysis.Compilation.GetEntryPoint retorna o IMethodSymboldo ponto de entrada do programa. Outros métodos permitem que você encontre qualquer símbolo de método em um projeto. Nesse objeto, podemos justificar o namespace contido (se houver um) e o tipo. O source neste exemplo é uma cadeia de caracteres interpolada que define o código-fonte a ser gerado, onde os espaços interpolados são preenchidos com o namespace contido e as informações de tipo. O source é adicionado ao context com o nome da dica. Neste exemplo, o gerador cria um novo arquivo de origem gerado que contém uma implementação do método partial no aplicativo de console. Você pode gravar geradores de origem para adicionar qualquer fonte desejada.

    Dica

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

  7. Agora temos um gerador em operação, mas precisamos conectá-lo ao nosso aplicativo de console. Edite o projeto do aplicativo de console original e adicione o seguinte, substituindo o caminho do projeto pelo do projeto .NET Standard criado acima:

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

    Essa nova referência não é uma referência de projeto tradicional e precisa ser editada manualmente para incluir os atributos OutputItemType e ReferenceOutputAssembly. Para obter mais informações sobre os atributos OutputItemType e ReferenceOutputAssembly de ProjectReference, consulte Itens comuns de projeto do MSBuild: ProjectReference.

  8. Agora, ao executar o aplicativo de console, você deverá ver que o código gerado é executado e impresso na tela. O aplicativo de console em si não implementa o método HelloFrom, em vez disso, é a fonte gerada durante a compilação do projeto Gerador de Origem. O texto a seguir é um exemplo de saída do aplicativo:

    Generator says: Hi from 'Generated Code'
    

    Observação

    Talvez seja necessário reiniciar o Visual Studio para ver o IntelliSense e se livrar de erros, pois a experiência de ferramentas está sendo aprimorada ativamente.

  9. Se você estiver usando o Visual Studio, poderá ver os arquivos gerados pela origem. Na janela Gerenciador de Soluções, expanda Dependência>Analisadores>SourceGenerator>SourceGenerator.HelloSourceGenerator e clique duas vezes no arquivo Program.g.cs.

    Visual Studio: arquivos gerados pela fonte do Gerenciador de Soluções.

    Quando você abrir esse arquivo gerado, o Visual Studio indicará que o arquivo é gerado automaticamente e que não pode ser editado.

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

  10. Você também pode definir propriedades de build para salvar o arquivo gerado e controlar onde os arquivos gerados são armazenados. No arquivo de projeto do aplicativo de console, adicione o elemento <EmitCompilerGeneratedFiles> a um <PropertyGroup> e defina seu valor como true. Compile o projeto novamente. Agora, os arquivos gerados são criados em obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator. Os componentes do mapa de caminho para a configuração de build, a estrutura de destino, o nome do projeto do gerador de origem e o nome de tipo totalmente qualificado do gerador. Você pode escolher uma pasta de saída mais conveniente adicionando o elemento <CompilerGeneratedFilesOutputPath> ao arquivo de projeto do aplicativo.

Próximas etapas

O Livro de Receitas de Geradores de Origem analisa alguns desses exemplos com algumas abordagens recomendadas para resolvê-los. Além disso, temos um conjunto de exemplos disponíveis no GitHub que você pode experimentar por conta própria.

Saiba mais sobre os Geradores de Origem nesses artigos: