CodeGym /Cursos /C# SELF /Source Generators e ...

Source Generators e System.Reflection.Emit

C# SELF
Nível 63 , Lição 4
Disponível

1. Introdução

Vamos ser honestos: a maioria das pessoas pensa em geração de código mais ou menos como numa máquina de fatiar ovos — útil, mas raramente essencial. Porém projetos .NET modernos ficam cada vez mais complexos, e automatizar trabalho rotineiro ou template do código não é só um "truque", é uma ferramenta importante para aumentar qualidade e produtividade.

Source Generators é um mecanismo que apareceu a partir do C# 9 e do .NET 5. Eles, como super-heróis na fase de compilação, podem gerar código C# que depois é compilado como parte do seu projeto. Ao mesmo tempo eles não mexem no código já compilado em runtime, apenas estendem o projeto com novos fontes antes da compilação.

Abaixo estão cenários típicos de uso de Source Generators.

Criação automática de código template

Em muitos projetos é preciso criar código repetitivo: construtores, métodos ToString, serializadores, property change notification (INotifyPropertyChanged) etc. Source Generators podem gerar esse código automaticamente, poupando desenvolvedores da rotina e do risco de erros de digitação.

Exemplo: Generator de ToString

Suponha que você desenvolva classes DTO (Data Transfer Object) para transferir dados. Você precisa implementar em cada classe um ToString significativo, que liste todas as propriedades.

Em vez de copiar manualmente, dá para criar um Source Generator que, para cada classe com o atributo [AutoToString], gere a implementação do método ToString. É o que fazem, por exemplo, bibliotecas como AutoToString.


// Sua classe com o atributo
[AutoToString] 
public partial class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Source Generator vai gerar (simplificado):
public partial class Person
{
    public override string ToString() => $"Person: Name={Name}, Age={Age}";
}

Vantagens: código legível e sempre atualizado quando novas propriedades são adicionadas.

Simplificação de serialização e desserialização

Source Generators são usados ativamente dentro da biblioteca padrão System.Text.Json para gerar código rápido de serialização e desserialização. Antes dos generators, serialização muitas vezes dependia de reflection (caro em performance), e agora o código gerado torna tudo rápido e seguro.

O que o desenvolvedor ganha? Você marca com [JsonSerializable(typeof(MyType))] — e o generator cria código performático de serialização para esse tipo.

Geração de código para configurações, mapeadores, contêineres DI

  • Configs: generators criam automaticamente classes de configuração com base em arquivos JSON.
  • Mapeadores: por exemplo, o projeto Mapster usa generators para gerar mapeamento entre tipos sem cópia manual de campos.
  • Dependency Injection: alguns containers (por exemplo, StrongInject) usam generators para gerar código de registro de serviços.

Integração com infra externa

Alguns generators analisam recursos externos (descrições de API, schemas GraphQL, protocolos Thrift etc.) e geram classes C# para trabalhar com eles. Isso evita atualizar código manualmente quando contratos mudam.

Checagens e diagnósticos em tempo de compilação

Source Generators podem ser usados também para checar código: "Se no projeto existe o método X, mas não foi satisfeita a condição Y — emitir warning no compilador!". Muitos linters e analyzers funcionam assim, e o generator pode até injetar um stub de código pronto.

2. Seu Source Generator para serialização automática

Vamos ver um exemplo simples "direto ao ponto": um generator que, para classes com o atributo [AutoJson], gera um método para serializar para JSON.

O código abaixo é só ilustração; na prática use System.Text.Json.SourceGeneration.


// Escrito manualmente:
[AutoJson]
public partial class Book
{
    public string Title { get; set; }
    public int Year { get; set; }
}

// O Generator vai adicionar:
public partial class Book
{
    public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}

Fluxo de interação entre Source Generator e o projeto

flowchart TD
    A(Seu Source Code) -->|O compilador chama| B(Source Generator)
    B -->|Código adicionado| C(Arquivos .cs novos)
    C --> D(Compilação do projeto)
  • Primeiro o compilador (Roslyn) analisa seus fontes.
  • Depois chama o seu generator (implementando ISourceGenerator).
  • O generator adiciona novos .cs que fazem parte da árvore de compilação.
  • No fim, a build contém tanto seu código quanto o gerado.

Tarefas especializadas: o que mais dá para gerar

  • Bindings para bibliotecas nativas (wrappers para código C ou WinAPI).
  • AOP: auto-logging de chamadas, aspectos (no estilo Fody, PostSharp etc.).
  • Descrição de API para geradores automáticos de documentação.
  • Processamento de recursos externos: SVG, SQL, Razor — o generator cria classes strongly-typed para acesso seguro por tipo.

3. Geração dinâmica de código com System.Reflection.Emit

Se Source Generators geram código C# antes da build, System.Reflection.Emit é a ferramenta para mágica em tempo de execução. Seu programa pode criar novos tipos, métodos e até assemblies — na hora!

Parece assustador? Um pouco. Mas às vezes é necessário: por exemplo, quando você escreve frameworks para proxy dinâmico (AOP, profiling, mocking), geradores de serializadores baseados em dados de runtime, ORMs dinâmicos, etc.

Quando usar Reflection.Emit

  • Tipos não são conhecidos antecipadamente (o usuário define estrutura em runtime).
  • Proxy dinâmico (wrappers para interceptar chamadas).
  • Serialização de alta performance (por exemplo, protobuf-net).
  • Plugins e engines de scripts com cenários complexos de carregamento.

O que dá para criar via Reflection.Emit

  • AssemblyBuilder — criar um novo assembly.
  • ModuleBuilder — módulo dentro do assembly.
  • TypeBuilder — descrever um novo tipo.
  • MethodBuilder — método com IL.
  • PropertyBuilder, FieldBuilder, EventBuilder — propriedades, campos, eventos.

Mini-exemplo: criar uma nova classe em runtime

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class DynamicTypeGenerator
{
    public static Type GenerateSimpleType(string typeName)
    {
        // 1. Criamos assembly e módulo
        var assemblyName = new AssemblyName("DynamicAssembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

        // 2. Criamos nova classe
        var typeBuilder = moduleBuilder.DefineType(
            typeName, 
            TypeAttributes.Public | TypeAttributes.Class
        );

        // 3. Adicionamos propriedade pública string Title
        var field = typeBuilder.DefineField("_title", typeof(string), FieldAttributes.Private);
        var prop = typeBuilder.DefineProperty("Title", PropertyAttributes.HasDefault, typeof(string), null);
        var getMethod = typeBuilder.DefineMethod("get_Title", MethodAttributes.Public, typeof(string), Type.EmptyTypes);
        var il = getMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);      // this
        il.Emit(OpCodes.Ldfld, field); // _title
        il.Emit(OpCodes.Ret);
        prop.SetGetMethod(getMethod);

        var setMethod = typeBuilder.DefineMethod("set_Title", MethodAttributes.Public, null, new[] { typeof(string) });
        il = setMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Stfld, field);
        il.Emit(OpCodes.Ret);
        prop.SetSetMethod(setMethod);

        // 4. Pronto! Criamos o Type
        return typeBuilder.CreateTypeInfo();
    }
}

Agora você pode usar esse tipo como um objeto C# comum, por exemplo via reflection:

var dynamicType = DynamicTypeGenerator.GenerateSimpleType("Book");
var obj = Activator.CreateInstance(dynamicType);
dynamicType.GetProperty("Title").SetValue(obj, "C# em retratos");
Console.WriteLine(dynamicType.GetProperty("Title").GetValue(obj)); // C# em retratos

É assim que nascem ORMs, proxies, serializadores, profilers e até alguns frameworks de teste.

4. Dicas úteis

Source Generators vs Reflection.Emit: quem é para quê?

Source Generators trabalham na compilação: deixam seu código-fonte mais esperto, e o resultado vai para a assembly compilada. Não servem para gerar código com base em dados obtidos durante a execução.

Reflection.Emit funciona em runtime: permite criar assemblies, tipos e métodos dinamicamente, mas esse código é mais difícil de depurar e manter.

Quando usar o quê?

Uma analogia do dia a dia:

  • Source Generators — como uma fábrica que produz peças prontas antes de montar o carro.
  • Reflection.Emit — como um engenheiro que solta um motor de foguete no carro enquanto você está dirigindo.

Particularidades e armadilhas

  • O código gerado pode ser difícil de depurar. Generators frequentemente têm opção para salvar fontes no disco — procure em obj\Generated.
  • Reflection.Emit cria assemblies em memória que não são descarregadas do AppDomain. Use AssemblyBuilderAccess.RunAndCollect para assemblies temporárias (se suportado).
  • Não abuse de Reflection.Emit para tarefas simples — às vezes um source generator ou um template de código comum é mais simples e confiável.
  • Generators exigem entendimento do Roslyn (para Source Generators) e IL (para Reflection.Emit).

Source Generators e Reflection.Emit

Critério Source Generators Reflection.Emit
Quando usado Na compilação Em runtime
Resultado Fontes C#, parte da assembly IL, novos tipos/assemblies
Cenários comuns Autogeração de código template, DI, mapeamento, serialização Proxies, ORMs dinâmicos, pipelines especiais em runtime
Complexidade de uso Média, requer conhecimento de Roslyn Alta, requer conhecimento de IL
Suporte da IDE e depuração Ótimo (fontes visíveis) Difícil
Performance Muito alta Pode ser alta

5. Cenários práticos

1. Geração de API strongly-typed

A organização fornece uma especificação OpenAPI. O generator analisa isso e cria classes de controllers, DTOs e código cliente para trabalhar com a API REST — tipado e com suporte a IntelliSense.

Código (pseudo):
// Spec: GET /users -> returns User[]
// Source Generator vai gerar:
public class ApiClient
{
    public Task<User[]> GetUsersAsync() { ... }
}

2. Injection/DI automático (Compile-time IoC)

Generators criam builders para registrar dependências e construir o grafo de objetos. Não é preciso escrever manualmente services.AddSingleton<IMyService, MyService>().

Código (pseudo):
[Injectable]
public class MyService : IMyService { ... }

// Source Generator vai gerar:
partial class DIContainer
{
    public void RegisterServices()
    {
        AddSingleton<IMyService, MyService>();
    }
}

3. Proxies dinâmicos — exemplo com Reflection.Emit

A biblioteca Castle DynamicProxy constroi tipos proxy que interceptam chamadas de método — base para AOP, logging, tracing, mocking.

Código (simplificado):
public interface IBookService { string GetBook(); }
public class BookService : IBookService { public string GetBook() => "C#"; }

var proxy = ProxyGenerator.CreateProxy<IBookService>(new BookService(), interceptor);
proxy.GetBook(); // A chamada é interceptada, dá para logar/modificar resultado

4. Serialização rápida sem reflection

Em vez de construir descrições de tipo em runtime via reflection (lento), Source Generators podem gerar código de serialização/desserialização antecipadamente — o mais rápido possível e sem overhead.

1
Pesquisa/teste
Reflexão, nível 63, lição 4
Indisponível
Reflexão
Reflexão e tipos dinâmicos
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION