1. Introducción
Cuando escribes código, seguro que a veces te has pillado pensando: «¿Por qué vuelvo a copiar el mismo patrón de código para distintas clases?» O: «¿Por qué la serialización, el logging, el mapeo de datos son tantas líneas repetitivas?» A veces apetece que alguien (o algo) escriba ese código repetitivo por ti.
Aquí es donde entran en escena los Source Generators — una funcionalidad nueva en C# que apareció en .NET 5 y que sigue evolucionando. Un Source Generator es una librería que se ejecuta en tiempo de compilación y puede generar dinámicamente código C# que se inyecta en tu proyecto antes del ensamblado final.
¿Para qué sirve?
- Automatización de la rutina: Te evita escribir clases/métodos repetitivos (boilerplate).
- Seguridad verificada por el compilador: El código generado se compila junto con el tuyo (a diferencia de T4 o la reflexión).
- Alto rendimiento: Serialización, DI, mapeo, etc. sin coste de reflexión en runtime.
- Soporte para patrones modernos: Implementación de enfoques que serían caros/difíciles sin generación de código.
¿Cómo funcionan los Source Generators «bajo el capó»?
Un Source Generator es una librería .NET (normalmente un proyecto tipo Class Library) que implementa la interfaz ISourceGenerator. Durante la compilación Roslyn ejecuta todos los generadores conectados, dándoles acceso al árbol sintáctico de tu código.
El generador analiza tu código, decide qué y dónde generar, y crea nuevos archivos C# que el compilador compila inmediatamente.
Generación automática de ToString
Empecemos con algo sencillo. Imagina que tienes una clase con muchas propiedades y necesitas implementar ToString. Escribiéndolo a mano queda así:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
=> $"Person(Name={Name}, Age={Age})";
}
Pero si las propiedades son muchas, da pereza y puedes olvidar actualizar algo. Un Source Generator puede hacerlo por ti.
2. ¿Cómo crear tu propio Source Generator?
Crear el proyecto
Abre JetBrains Rider o Visual Studio, crea un nuevo proyecto tipo Class Library (.NET Standard) — esos proyectos pueden ser generadores. Luego añade los paquetes NuGet:
- Microsoft.CodeAnalysis.CSharp
- Microsoft.CodeAnalysis.Analyzers
Atributos importantes
- [Generator] — indica que esa clase es un Source Generator.
Plantilla mínima del generador
Aquí tienes un ejemplo mínimo que funciona:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// Se pueden registrar acciones adicionales (opcional)
}
public void Execute(GeneratorExecutionContext context)
{
var code = @"
namespace Generated
{
public static class HelloWorld
{
public static string SayHello() => ""Hello, world! I am generated!"";
}
}";
context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
}
}
Este generador sencillo siempre añade la clase estática HelloWorld con el método SayHello durante la compilación.
¿Cómo usar Source Generators en la aplicación principal?
Conecta el proyecto generador como paquete NuGet o Project Reference en la sección Analyzer (más detalles en la documentación oficial).
El código generado está disponible inmediatamente en tu proyecto — no hace falta nada más, solo úsalo:
// ¡Esto se generará automáticamente!
using Generated;
Console.WriteLine(HelloWorld.SayHello());
3. Ejemplo real: generación automática de ToString
Supongamos que queremos que todas las clases marcadas con el atributo [AutoToString] obtengan automáticamente la implementación del método ToString. Para eso necesitamos:
- Crear tu propio atributo.
- Analizar todas las clases con ese atributo.
- Para cada clase así, generar el método ToString.
Atributo
[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}
Uso en el código
[AutoToString]
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
Lógica simple de generación
El generador buscará clases con [AutoToString] y generará algo parecido a esto:
public override string ToString()
=> $"Product(Name={Name}, Price={Price})";
Un fragmento real del generador
La idea básica es recorrer el árbol sintáctico con Roslyn:
public void Execute(GeneratorExecutionContext context)
{
// Analizamos todos los árboles sintácticos
foreach (var tree in context.Compilation.SyntaxTrees)
{
var root = tree.GetRoot();
// Buscamos todas las clases con el atributo necesario (¡ejemplo simplificado!)
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;
// Obtenemos todas las propiedades de la clase
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));
}
}
}
Ten en cuenta: para código de production se usa un análisis más correcto vía SemanticModel de Roslyn.
4. Detalles útiles
En qué fijarse
Los Source Generators no pueden modificar el código fuente existente — solo crear nuevos archivos (por ejemplo, clases partial adicionales, métodos, etc.). Esto significa que si tu clase está declarada como partial, puedes generar métodos o propiedades adicionales para ella.
A veces es complejo parsear correctamente la sintaxis y manejar todos los matices del lenguaje (clases anidadas, genéricos, modificadores, etc.). El autor del generador debe asegurarse de que el código generado compile y no rompa el proyecto.
Otra trampa: si generas métodos que implementan una interfaz, asegúrate de que los archivos se generen en cada compilación. Si no, pueden aparecer errores raros de compilación. Las herramientas modernas suelen manejar esto, pero es algo a recordar.
Source Generators vs. reflexión
Reflexión: se ejecuta en runtime, es costosa en recursos, no está verificada por el compilador y suele ser lenta con grandes volúmenes de datos.
Source Generator: genera código en tiempo de compilación. Todo se verifica estáticamente, el IDE ve los métodos, funciona el autocompletado y el rendimiento es como el de código C# normal.
Utilidad práctica
- System.Text.Json: generación de serialización/deserialización sin reflexión.
- Diseño de contenedores DI: por ejemplo, Microsoft.Extensions.DependencyInjection con generación del grafo de dependencias.
- Mapeadores tipo Mapster: pasar de reflexión a generación compile-time de código de mapping.
- Frameworks de testing: autogeneración de métodos de test a partir de atributos.
- ASP.NET Minimal APIs (desde .NET 7): generación de handlers para endpoints.
Configuración, parámetros y opciones
Los generadores se pueden configurar mediante parámetros de MSBuild, archivos adicionales y convenciones. Por ejemplo, puedes generar distintos ToString según el entorno (Debug/Release) o la configuración de la aplicación.
Cómo los Source Generators se relacionan con tareas reales
Para un desarrollador que busca código limpio y rápido, es una herramienta excelente: menos rutina, más comprobaciones en compile-time, ayuda del IDE y refactorizaciones claras. Conocimientos sobre generadores se piden cada vez más en entrevistas — desde serialización y DI hasta mapeo.
Ciclo de vida de un Source Generator
| Etapa | Qué ocurre |
|---|---|
| 1. Proyecto conectado | Tu generador está añadido como analyzer/reference |
| 2. Roslyn compila la fuente | El generador recibe el AST (árbol sintáctico abstracto) |
| 3. El generador se ejecuta | Añade nuevos archivos .cs para compilar |
| 4. Todo se compila | Los archivos generados pasan a formar parte de tu ensamblado |
| 5. ¡El código listo! | Los métodos/clases generados están disponibles para invocar |
5. Depuración y errores típicos
Uno de los errores más frecuentes de los que empiezan con generadores es olvidar la palabra clave partial en la clase a la que quieres añadir código. Si no pones partial, el compilador no verá tus cambios. A veces el archivo generado puede no aparecer en la IDE hasta la primera recompilación — no te alarmes.
Ten cuidado al nombrar los archivos generados: si das a todos el mismo Name, se sobrescribirán. Un truco es incluir el nombre de la clase en el nombre del archivo, por ejemplo context.AddSource($"{className}_ToString", ...).
Conflicto por importar dos veces atributos — si generas una clase con un atributo que ya existe en el proyecto principal, habrá conflicto. Es mejor poner los atributos necesarios en un proyecto común o generar el código sólo cuando hace falta.
GO TO FULL VERSION