.NET Source Generators C# Compilador Performance Metaprogramação

Source Generators no .NET: geração de código em tempo de compilação sem reflection

Guia prático de Source Generators no .NET: IIncrementalGenerator vs ISourceGenerator, casos de uso reais (mapeamento, serialização, validação).

N
Neryx Digital Architects
10 de fevereiro de 2026
13 min de leitura
230 profissionais leram
Categoria: .NET Público: Times de engenharia e produto Etapa: Aprendizado

Reflection é poderosa, mas tem um custo: resolve tipos e membros em runtime, executa em cada chamada, e dificulta AOT compilation (Native AOT, Blazor WebAssembly). Source Generators oferecem uma alternativa: inspecionam o código-fonte durante a compilação e geram código C# adicional que é incluído no build como se você tivesse escrito à mão. Zero overhead de runtime, compatível com AOT, e completamente tipado.

O System.Text.Json, o logging de alta performance (LoggerMessage), e o Refit já usam Source Generators internamente. Este guia mostra como escrever os seus.

ISourceGenerator vs IIncrementalGenerator

Existem duas APIs para Source Generators: a original (ISourceGenerator) e a incremental (IIncrementalGenerator), introduzida no .NET 6. A Microsoft recomenda fortemente a versão incremental para todos os generators novos — a diferença de desempenho no compilador é significativa.

O ISourceGenerator roda o generator completo a cada mudança no arquivo. O IIncrementalGenerator usa um modelo de pipeline funcional com caching: cada etapa da análise é cacheada separadamente, e o generator só reexecuta a parte que depende do que mudou. Em projetos grandes, isso reduz o tempo de build incremental por ordens de magnitude.

Setup: projeto de generator

# Source generators precisam de um projeto de biblioteca separado
dotnet new classlib -n MyApp.Generators
cd MyApp.Generators

# Referência ao Roslyn (compilador) — apenas como analyzer, não como runtime dep
dotnet add package Microsoft.CodeAnalysis.CSharp
<!-- MyApp.Generators.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <Nullable>enable</Nullable>
    <LangVersion>latest</LangVersion>
    <!-- Importante: generators são analyzers, não bibliotecas de runtime -->
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.*"
                      PrivateAssets="all" />
  </ItemGroup>
</Project>

<!-- No projeto que usa o generator -->
<!-- MyApp.csproj -->
<ItemGroup>
  <ProjectReference Include="..\MyApp.Generators\MyApp.Generators.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

Exemplo 1: gerador de ToString() automático

// MyApp.Generators/ToStringGenerator.cs
// Gera ToString() para qualquer classe marcada com [GenerateToString]

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Text;

[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 1. Injeta o atributo [GenerateToString] no projeto consumidor
        context.RegisterPostInitializationOutput(ctx =>
            ctx.AddSource("GenerateToStringAttribute.g.cs", """
                namespace MyApp;

                [System.AttributeUsage(System.AttributeTargets.Class)]
                public sealed class GenerateToStringAttribute : System.Attribute { }
                """));

        // 2. Pipeline incremental: filtra apenas classes com [GenerateToString]
        var provider = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "MyApp.GenerateToStringAttribute",
                predicate: static (node, _) => node is ClassDeclarationSyntax,
                transform: static (ctx, _) => GetClassInfo(ctx))
            .Where(static m => m is not null)
            .Collect();

        // 3. Registra a geração de código
        context.RegisterSourceOutput(provider, static (spc, classes) =>
        {
            foreach (var classInfo in classes)
            {
                if (classInfo is null) continue;
                var source = GenerateToStringMethod(classInfo);
                spc.AddSource($"{classInfo.ClassName}.ToString.g.cs", source);
            }
        });
    }

    private static ClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext ctx)
    {
        if (ctx.TargetSymbol is not INamedTypeSymbol classSymbol)
            return null;

        var properties = classSymbol.GetMembers()
            .OfType<IPropertySymbol>()
            .Where(p => p.DeclaredAccessibility == Accessibility.Public)
            .Select(p => p.Name)
            .ToImmutableArray();

        return new ClassInfo(
            classSymbol.ContainingNamespace.ToDisplayString(),
            classSymbol.Name,
            properties);
    }

    private static string GenerateToStringMethod(ClassInfo classInfo)
    {
        var sb = new StringBuilder();
        sb.AppendLine($"namespace {classInfo.Namespace};");
        sb.AppendLine();
        sb.AppendLine($"partial class {classInfo.ClassName}");
        sb.AppendLine("{");
        sb.Append("    public override string ToString() => ");
        sb.Append($"$\"{classInfo.ClassName} {{ ");

        var parts = classInfo.Properties
            .Select(p => $"{p} = {{{p}}}");
        sb.Append(string.Join(", ", parts));

        sb.AppendLine(" }}\";");
        sb.AppendLine("}");

        return sb.ToString();
    }

    private record ClassInfo(
        string Namespace,
        string ClassName,
        ImmutableArray<string> Properties);
}
// Uso no projeto consumidor:
[GenerateToString]
public partial class Order    // precisa ser partial para o generator poder adicionar métodos
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; } = "";
    public decimal Total { get; set; }
}

// O generator produz automaticamente:
// Order.ToString.g.cs:
// public override string ToString() =>
//     $"Order { Id = {Id}, CustomerName = {CustomerName}, Total = {Total} }";

Exemplo 2: gerador de mapper sem AutoMapper

// Gera métodos de mapeamento entre entidades e DTOs sem reflection em runtime

[Generator]
public class MapperGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(ctx =>
            ctx.AddSource("MapFromAttribute.g.cs", """
                namespace MyApp.Mapping;

                [System.AttributeUsage(System.AttributeTargets.Class)]
                public sealed class MapFromAttribute : System.Attribute
                {
                    public System.Type SourceType { get; }
                    public MapFromAttribute(System.Type sourceType)
                        => SourceType = sourceType;
                }
                """));

        var provider = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                "MyApp.Mapping.MapFromAttribute",
                predicate: (node, _) => node is ClassDeclarationSyntax,
                transform: GetMapperInfo)
            .Where(m => m is not null)
            .Collect();

        context.RegisterSourceOutput(provider, GenerateMappers);
    }

    private static MapperInfo? GetMapperInfo(GeneratorAttributeSyntaxContext ctx, CancellationToken ct)
    {
        if (ctx.TargetSymbol is not INamedTypeSymbol targetSymbol)
            return null;

        var attribute = ctx.Attributes.FirstOrDefault();
        if (attribute?.ConstructorArguments.FirstOrDefault().Value
            is not INamedTypeSymbol sourceSymbol)
            return null;

        // Encontra propriedades que existem em ambos (por nome e tipo compatível)
        var targetProps = targetSymbol.GetMembers().OfType<IPropertySymbol>()
            .Where(p => p.SetMethod != null)
            .ToImmutableArray();

        var sourceProps = sourceSymbol.GetMembers().OfType<IPropertySymbol>()
            .Where(p => p.GetMethod != null)
            .ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);

        var mappings = targetProps
            .Where(tp => sourceProps.ContainsKey(tp.Name))
            .Select(tp => new PropertyMapping(tp.Name, tp.Name))
            .ToImmutableArray();

        return new MapperInfo(
            targetSymbol.ContainingNamespace.ToDisplayString(),
            targetSymbol.Name,
            sourceSymbol.ToDisplayString(),
            mappings);
    }

    private static void GenerateMappers(
        SourceProductionContext spc,
        ImmutableArray<MapperInfo?> mappers)
    {
        foreach (var mapper in mappers)
        {
            if (mapper is null) continue;

            var sb = new StringBuilder();
            sb.AppendLine($"namespace {mapper.Namespace};");
            sb.AppendLine();
            sb.AppendLine($"partial class {mapper.TargetClass}");
            sb.AppendLine("{");
            sb.AppendLine($"    public static {mapper.TargetClass} From({mapper.SourceType} source)");
            sb.AppendLine("        => new()");
            sb.AppendLine("        {");

            foreach (var m in mapper.Mappings)
                sb.AppendLine($"            {m.TargetProp} = source.{m.SourceProp},");

            sb.AppendLine("        };");
            sb.AppendLine("}");

            spc.AddSource($"{mapper.TargetClass}.Mapper.g.cs", sb.ToString());
        }
    }

    private record MapperInfo(string Namespace, string TargetClass, string SourceType,
        ImmutableArray<PropertyMapping> Mappings);
    private record PropertyMapping(string TargetProp, string SourceProp);
}
// Uso:
[MapFrom(typeof(Order))]
public partial class OrderDto
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; } = "";
    public decimal Total { get; set; }
}

// Generator produz OrderDto.Mapper.g.cs:
// public static OrderDto From(Order source) => new()
// {
//     Id = source.Id,
//     CustomerName = source.CustomerName,
//     Total = source.Total,
// };

// Uso sem reflection, com IntelliSense completo:
var dto = OrderDto.From(order);

Testes unitários de generators

dotnet add package Microsoft.CodeAnalysis.CSharp.Testing
dotnet add package Microsoft.CodeAnalysis.Testing
public class ToStringGeneratorTests
{
    [Fact]
    public async Task Generator_ClassWithAttribute_GeneratesToStringMethod()
    {
        var source = """
            using MyApp;

            [GenerateToString]
            public partial class Product
            {
                public string Name { get; set; } = "";
                public decimal Price { get; set; }
            }
            """;

        var expected = """
            namespace ;

            partial class Product
            {
                public override string ToString() =>
                    $"Product { Name = {Name}, Price = {Price} }";
            }
            """;

        await new CSharpSourceGeneratorTest<ToStringGenerator, DefaultVerifier>
        {
            TestState =
            {
                Sources = { source },
                GeneratedSources =
                {
                    (typeof(ToStringGenerator), "Product.ToString.g.cs", expected)
                }
            }
        }.RunAsync();
    }
}

Depurando generators no Visual Studio e Rider

// Para depurar um generator, adicione no início do Initialize():
System.Diagnostics.Debugger.Launch();

// Ou configure launch profile no projeto consumidor.
// Outra opção: inspecionar o código gerado sem depurar:
<!-- No .csproj do projeto consumidor — salva os arquivos gerados em disco -->
<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

<ItemGroup>
  <!-- Exclui da compilação (já são incluídos pelo generator) -->
  <Compile Remove="Generated\**\*" />
</ItemGroup>

Quando usar Source Generators

Source Generators brilham quando você tem código repetitivo que segue um padrão derivável de metadados: mapeamento entre tipos, serialização customizada, geração de proxies, registro automático de services, implementações de interfaces baseadas em atributos. Se você se pega escrevendo o mesmo padrão de código dezenas de vezes e pensando "isso poderia ser gerado", provavelmente é um bom candidato.

Evite Source Generators para lógica de negócio ou quando a geração depende de dados externos (banco de dados, APIs). Generators rodam no compilador — devem ser determinísticos e rápidos. Um generator lento ou com side effects degrada a experiência de desenvolvimento do time inteiro.


Source Generators são uma das funcionalidades mais avançadas do ecossistema .NET moderno. Se você quer explorar metaprogramação e otimizações de compilação no seu projeto, a Neryx pode ajudar.

Quer transformar esse aprendizado em plano de ação?

Se o tema deste artigo se parece com o momento do seu time, podemos ajudar a decidir o próximo passo com clareza.

Falar com um especialista

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.