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.