1. Introdução
Quando você escreve código, provavelmente às vezes já pensou: «Por que eu copio o mesmo padrão de código para várias classes?» Ou: «Por que serialização, logging, mapeamento de dados — tudo isso são tantas linhas repetitivas?» Às vezes dá vontade que alguém (ou alguma coisa) escreva esse template trabalhoso por você.
É aí que entram os Source Generators — uma funcionalidade nova no C#, aparecida no .NET 5 e que continua evoluindo. Um Source Generator é uma biblioteca que roda em tempo de compilação e pode gerar dinamicamente código C# que é automaticamente incluído no seu projeto antes da build final.
Por que isso é útil?
- Automatiza a rotina: Evita escrever classes/métodos repetitivos (boilerplate).
- Segurança checada pelo compilador: O código gerado é compilado junto com o seu (diferente de T4 ou reflexão).
- Alta performance: Serialização, DI, mapeamento etc. sem custo de reflection em runtime.
- Suporte a padrões modernos: Implementa abordagens que seriam difíceis/caras sem geração de código.
Como os Source Generators funcionam "por baixo do capô"?
Source Generator é uma biblioteca .NET (normalmente um projeto do tipo Class Library) que implementa a interface ISourceGenerator. Durante a compilação o Roslyn executa todos os geradores conectados, dando a eles acesso à árvore de sintaxe do seu código.
O gerador analisa seu código, decide o que e onde gerar, e cria novos arquivos C# que o compilador logo compila.
Geração automática de ToString
Vamos começar com algo simples. Imagine que temos uma classe com muitas propriedades e precisamos implementar o ToString. Escrever à mão fica assim:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
=> $"Person(Name={Name}, Age={Age})";
}
Mas se as propriedades ficam muitas — dá preguiça e dá para esquecer de atualizar algo. O Source Generator pode fazer isso por você!
2. Como criar seu Source Generator?
Criando o projeto
Abra o JetBrains Rider ou o Visual Studio, crie um novo projeto do tipo Class Library (.NET Standard) — são esses projetos que podem ser geradores. Depois adicione os pacotes NuGet:
- Microsoft.CodeAnalysis.CSharp
- Microsoft.CodeAnalysis.Analyzers
Atributos importantes
- [Generator] — indica que essa classe é um Source Generator.
Template mínimo do gerador
Aqui vai um exemplo mínimo funcional:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// Dá para registrar ações adicionais (opcional)
}
public void Execute(GeneratorExecutionContext context)
{
var code = @"
namespace Generated
{
public static class HelloWorld
{
public static string SayHello() => ""Privet, mir! Ya sgenerirovan!"";
}
}";
context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
}
}
Esse gerador simples sempre adiciona a classe estática HelloWorld com o método SayHello durante a compilação.
Como usar Source Generators no app principal?
Referencie o projeto-gerador como pacote NuGet ou Project Reference na seção Analyzer (veja — documentação oficial).
O código gerado já fica disponível no seu projeto — não precisa conectar nada extra, só use:
// Isso será gerado automaticamente!
using Generated;
Console.WriteLine(HelloWorld.SayHello());
3. Exemplo real: geração automática de ToString
Vamos supor que queremos que todas as classes marcadas com o atributo [AutoToString] recebam automaticamente a implementação do método ToString. Para isso precisamos:
- Criar um atributo.
- Analisar todas as classes com esse atributo.
- Para cada classe gerada, criar o método ToString.
Atributo
[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}
Uso no código
[AutoToString]
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
Logica simples de geração
O gerador vai procurar classes com [AutoToString] e gerar algo como:
public override string ToString()
=> $"Product(Name={Name}, Price={Price})";
Trecho real do código do gerador
A ideia básica — percorrer a árvore de sintaxe com Roslyn:
public void Execute(GeneratorExecutionContext context)
{
// Analisamos todas as árvores de sintaxe
foreach (var tree in context.Compilation.SyntaxTrees)
{
var root = tree.GetRoot();
// Procuramos todas as classes com o atributo desejado (exemplo simples!)
var classes = root.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Where(c => c.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().Contains("AutoToString")));
foreach (var @class in classes)
{
var className = @class.Identifier.Text;
// Pegamos todas as propriedades da classe
var props = @class.Members
.OfType<PropertyDeclarationSyntax>()
.Select(p => p.Identifier.Text)
.ToArray();
var toStringCode = string.Join(", ", props.Select(p => $"{p}={{this.{p}}}"));
var generated = $@"
partial class {className}
{{
public override string ToString() => $""{className}({toStringCode})"";
}}";
context.AddSource($"{className}_ToString", SourceText.From(generated, Encoding.UTF8));
}
}
}
Repare: para código de produção é melhor fazer uma análise mais correta usando o SemanticModel do Roslyn.
4. Dicas úteis
O que prestar atenção
Source Generators não conseguem modificar código-fonte existente — apenas criar novos arquivos (por exemplo, classes/métodos partial adicionais). Isso significa que se sua classe for declarada como partial, você pode gerar métodos ou propriedades adicionais para ela.
Às vezes é complicado interpretar corretamente a sintaxe e cobrir todos os cenários da linguagem (classes aninhadas, generics, modificadores etc.). O autor do gerador precisa garantir que o código gerado seja compilável e não quebre o projeto.
Outra armadilha — se você gera métodos que implementam uma interface, certifique-se de que os arquivos são gerados a cada build. Caso contrário podem surgir erros estranhos de compilação. Ferramentas modernas costumam lidar com isso, mas é bom ficar atento.
Source Generators vs. reflection
Reflection: roda em runtime, custoso em recursos, não é verificado pelo compilador, e costuma ser lento em grandes volumes de dados.
Source Generator: gera código em tempo de compilação. Tudo é checado estaticamente, a IDE vê os métodos, autocomplete funciona, e a performance é como a de código C# normal.
Benefícios práticos
- System.Text.Json: geração de serialização/deserialização sem reflection.
- Projeto de containers DI: por exemplo, Microsoft.Extensions.DependencyInjection com geração do grafo de dependências.
- Mapeadores como Mapster: troca de reflection por geração de código de mapping em compile-time.
- Frameworks de teste: geração automática de métodos de teste baseada em atributos.
- ASP.NET Minimal APIs (a partir do .NET 7): geração de endpoint handlers.
Configuração, parâmetros e opções
Geradores podem ser configurados via parâmetros do MSBuild, arquivos adicionais e convenções. Dá para, por exemplo, gerar diferentes ToString dependendo do ambiente (Debug/Release) ou da configuração da aplicação.
Como os Source Generators se relacionam com tarefas reais
Para um dev que busca código limpo e rápido, é uma ferramenta excelente: menos rotina, mais checagens em compile-time, sugestões da IDE e refatoração segura. Conhecimento sobre geradores tem aparecido mais em entrevistas — desde serialização e DI até mapeamento.
Ciclo de vida de um Source Generator
| Fase | O que acontece |
|---|---|
| 1. Projeto referenciado | Seu gerador é adicionado como analyzer/reference |
| 2. Roslyn compila o código | O gerador recebe o AST (árvore sintática abstrata) |
| 3. Gerador executa | Adiciona novos arquivos .cs para compilação |
| 4. Tudo é compilado | Os arquivos gerados viram parte do seu assembly |
| 5. Código pronto! | Os métodos/classes gerados estão disponíveis para uso |
5. Debug e erros típicos
Um dos erros mais comuns de quem começa a escrever geradores é esquecer a palavra-chave partial na classe onde você quer acrescentar código. Se você não marcar como partial, o compilador simplesmente não verá suas alterações. Às vezes o arquivo gerado pode não aparecer na IDE até a primeira rebuild — não se assuste.
Cuidado ao nomear os arquivos gerados: se você der o mesmo Name para todos, eles vão sobrescrever uns aos outros. Dica: incluir o nome da classe no nome do arquivo ajuda — por exemplo, context.AddSource($"{className}_ToString", ...).
Erro de import duplicado de atributos — se você gerar uma classe com um atributo que já existe no projeto principal, vai bater conflito. Melhor extrair atributos necessários para um projeto comum ou só gerar o atributo quando realmente necessário.
GO TO FULL VERSION