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

Source Generators y System.Reflection.Emit

C# SELF
Nivel 63 , Lección 4
Disponible

1. Introducción

Seamos honestos: la mayoría de la gente piensa en la generación de código más o menos como en una máquina para cortar huevos — útil, pero raramente imprescindible. Sin embargo, los proyectos .NET modernos se vuelven cada vez más complejos, y automatizar trabajo rutinario o repetitivo en el código no es solo una "feature", sino una herramienta importante para mejorar la calidad y la productividad.

Source Generators es un mecanismo que apareció a partir de C# 9 y .NET 5. Son como superhéroes en la etapa de compilación: pueden generar código C# que luego se compila como parte de tu proyecto. No interfieren con el código ensamblado ya compilado en tiempo de ejecución, solo amplían el proyecto con nuevos fuentes antes de la compilación.

A continuación veremos escenarios típicos para usar Source Generators.

Creación automática de código repetitivo

En muchos proyectos toca crear código del mismo tipo: constructores, métodos ToString, serializadores, property change notification (INotifyPropertyChanged), etc. Source Generators pueden generar ese código automáticamente, ahorrando a los desarrolladores la rutina y el riesgo de errores tipográficos.

Ejemplo: generador de ToString

Imagina que desarrollas clases DTO (Data Transfer Object) para transferir datos. Necesitas implementar en cada clase un ToString significativo que liste todas las propiedades.

En lugar de copiar manualmente, puedes crear un Source Generator que para cada clase con el atributo [AutoToString] genere la implementación del método ToString. Así lo hacen, por ejemplo, librerías como AutoToString.


// Tu clase con el atributo
[AutoToString] 
public partial class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
}

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

Ventajas: el código es legible y siempre se actualiza al añadir nuevas propiedades.

Simplificación de serialización y deserialización

Source Generators se usan activamente dentro de la biblioteca estándar System.Text.Json para generar código rápido de serialización/deserialización. Antes de los generators, la serialización a menudo requería reflection (cara en rendimiento), y ahora el código generado hace todo rápido y seguro.

¿Qué obtiene el desarrollador? Indicas [JsonSerializable(typeof(MyType))] — y el generador crea código de serialización de alto rendimiento para ese tipo.

Generación de código para configuraciones, mapeadores, contenedores DI

  • Configs: los generators crean automáticamente clases de configuración basadas en archivos JSON.
  • Mapeadores: por ejemplo, el proyecto Mapster usa generators para crear mapeos entre tipos sin copiar campos manualmente.
  • Dependency Injection: algunos contenedores (por ejemplo, StrongInject) usan generators para generar código de registro de servicios.

Integración con infraestructura externa

Algunos generators analizan recursos externos (descripciones de API, esquemas GraphQL, protocolos Thrift, etc.) y generan clases C# para trabajar con ellos. Esto evita actualizar manualmente el código cuando cambian los contratos.

Chequeos y diagnósticos en tiempo de compilación

Source Generators también se pueden usar para validar código: "Si en el proyecto aparece el método X, pero no se cumple la condición Y — advertencia en el compilador!". Así funcionan muchos linters y analyzers, pero el generator puede añadir sus mensajes o incluso inyectar código stub listo.

2. Tu propio Source Generator para serialización automática

Veamos un ejemplo simple "a lo bruto": un generator que para una clase con el atributo [AutoJson] genera un método para serializar a formato JSON.

El código abajo es solo ilustrativo; en la práctica usa System.Text.Json.SourceGeneration.


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

// El generator añadirá:
public partial class Book
{
    public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}

Esquema de interacción entre Source Generator y el proyecto

flowchart TD
    A(Tu Source Code) -->|El compilador invoca| B(Source Generator)
    B -->|Código añadido| C(Nuevos archivos .cs)
    C --> D(Compilación del proyecto)
  • Primero el compilador (Roslyn) analiza tus fuentes.
  • Luego invoca "tu" generator (que implementa ISourceGenerator).
  • El generator añade nuevos archivos .cs, que forman parte del árbol de compilación.
  • Al final la build contiene tu código y el código generado.

Tareas especializadas: qué más se puede generar

  • Bindings para librerías nativas (wrappers para código C o WinAPI).
  • AOP: auto-logging de llamadas, aspectos (estilo Fody, PostSharp, etc.).
  • Descripción de API para generadores automáticos de documentación.
  • Procesamiento de recursos externos: SVG, SQL, Razor — el generator crea clases strongly-typed para acceso seguro por tipo.

3. Generación dinámica de código con System.Reflection.Emit

Si Source Generators generan código C# antes de la build, System.Reflection.Emit es la herramienta para magia en runtime. Tu aplicación puede crear tipos, métodos e incluso assemblies por sí misma — ¡al vuelo!

¿Suena intimidante? Un poco. Pero a veces es necesario: por ejemplo, cuando escribes frameworks para proxy dinámico (AOP, profiling, mocking), generators de serializadores basados en datos de runtime, ORM dinámicos, etc.

Cuándo usar Reflection.Emit

  • Los tipos no se conocen por adelantado (el usuario define la estructura en tiempo de ejecución).
  • Proxy dinámico (wrappers para interceptar llamadas).
  • Serialización de alto rendimiento (por ejemplo, protobuf-net).
  • Plugins y motores de scripting con escenarios complejos de carga.

Qué puedes crear con Reflection.Emit

  • AssemblyBuilder — crear un nuevo assembly.
  • ModuleBuilder — módulo dentro del assembly.
  • TypeBuilder — descripción de un nuevo tipo.
  • MethodBuilder — método con IL.
  • PropertyBuilder, FieldBuilder, EventBuilder — propiedades, campos, eventos.

Mini-ejemplo: crear una clase al vuelo

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

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

        // 2. Creamos la nueva clase
        var typeBuilder = moduleBuilder.DefineType(
            typeName, 
            TypeAttributes.Public | TypeAttributes.Class
        );

        // 3. Añadimos una propiedad 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. ¡Listo! Creamos el Type
        return typeBuilder.CreateTypeInfo();
    }
}

Ahora puedes usar ese tipo como un objeto C# normal, por ejemplo mediante reflection:

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

Así nacen ORM, proxies, serializadores, profilers y hasta algunos frameworks de testing.

4. Nitos útiles

Source Generators vs Reflection.Emit: ¿quién para qué?

Source Generators trabajan en la etapa de compilación: hacen tu código fuente más "listo", y el resultado entra en el assembly compilado. No sirven para generar código basado en datos obtenidos durante la ejecución.

Reflection.Emit funciona en runtime: permite crear assemblies, tipos y métodos dinámicamente, pero ese código es más difícil de depurar y mantener.

¿Cuándo usar cada uno?

Una analogía:

  • Source Generators — como una fábrica que saca piezas listas antes del montaje del coche.
  • Reflection.Emit — como un ingeniero que suelda un motor cohete al coche mientras vas conduciendo.

Particularidades y trampas

  • El código generado puede ser difícil de depurar. Los generators suelen poder volcar fuentes en disco — búscalos en la carpeta obj\Generated.
  • Reflection.Emit crea assemblies en memoria que no se descargan del AppDomain. Usa AssemblyBuilderAccess.RunAndCollect para assemblies temporales (si está soportado).
  • No abuses de Reflection.Emit para tareas sencillas — a veces un Source Generator o un patrón de plantilla es más simple y fiable.
  • Los generators requieren entender Roslyn (para Source Generators) y IL (para Reflection.Emit).

Source Generators y Reflection.Emit

Criterio Source Generators Reflection.Emit
Se usa En la etapa de compilación En tiempo de ejecución
Resultado Fuentes C#, parte del assembly IL, nuevos tipos/assemblies
Escenarios comunes Autogeneración de código repetitivo, DI, mapeo, serialización Proxies, ORM dinámicos, pipelines especiales en runtime
Dificultad de uso Media, necesitas Roslyn Alta, necesitas IL
Soporte IDE y depuración Excelente (ves las fuentes) Complicado
Rendimiento Muy alto Puede ser alto

5. Escenarios prácticos

1. Generación de API strongly-typed

La organización entrega una especificación OpenAPI. Un generator la analiza y crea clases de controladores, DTOs y código cliente para trabajar con el API REST — tipado y con soporte de IntelliSense.

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

2. Inyección/DI automática (Compile-time IoC)

Los generators crean builders para registrar dependencias y construir el grafo de objetos. No necesitas escribir manualmente services.AddSingleton<IMyService, MyService>().

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

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

3. Proxies dinámicos — ejemplo con Reflection.Emit

La librería Castle DynamicProxy construye tipos proxy que interceptan llamadas a métodos — 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(); // La llamada es interceptada, puedes loggear/modificar el resultado

4. Serialización rápida sin reflection

En vez de construir descripciones de tipo en runtime con reflection (lento), Source Generators pueden generar código de serialización/deserialización por adelantado — lo más rápido posible y sin overhead.

1
Cuestionario/control
Reflexión, nivel 63, lección 4
No disponible
Reflexión
Reflexión y tipos dinámicos
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION