Un Source Generator es una pieza de código que se ejecuta durante la compilación. Recibe el árbol sintáctico completo del proyecto y puede generar archivos .cs adicionales que se compilan junto con el resto del código. No son templates ni herramientas de scaffolding: tienen acceso a toda la información semántica del proyecto en el momento en que el compilador la tiene.
La idea es sencilla: hay tareas que hacemos en runtime, como inspeccionar tipos con reflection para serializar objetos o mapear propiedades, que en realidad se podrían hacer en compilación. Ya sabemos qué tipos tiene el proyecto. ¿Por qué no generar el código de serialización una sola vez, cuando compilamos, en lugar de calcular lo mismo cada vez que arranca la aplicación?
Eso es exactamente lo que permiten los Source Generators: mover trabajo de runtime a compilación.
El coste de la reflection y por qué importa
Cuando System.Text.Json serializa un objeto usando reflection, tiene que inspeccionar los tipos en tiempo de ejecución para saber qué propiedades existen, qué tipos tienen y cómo convertirlas a JSON. Ese trabajo ocurre en cada arranque de la aplicación, y en el caso de funciones serverless o aplicaciones que se inician con frecuencia, suma.
Con Source Generation, el generator analiza los tipos durante la compilación y genera el código de serialización directamente. En runtime no hay reflection: el código ya sabe exactamente cómo serializar cada tipo porque ese código existe en el ensamblado.
La diferencia tiene dos consecuencias prácticas. Primera, el startup es más rápido porque no hay que descubrir nada. Segunda, el código es compatible con publicación AOT y NativeAOT, que directamente prohíben la reflection dinámica.
Para una aplicación web en un servidor dedicado, la ganancia puede ser pequeña. Para una función Lambda que arranca en frío cada pocos segundos, o para una aplicación publicada con PublishAot=true, la diferencia es real.
System.Text.Json Source Generation: el ejemplo más conocido
El caso más habitual con el que te vas a cruzar en proyectos .NET actuales es la source generation de System.Text.Json. El patrón es el siguiente: declaras una clase de contexto parcial decorada con el atributo [JsonSerializable] para cada tipo que quieras serializar.
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(Order))]
internal partial class AppJsonContext : JsonSerializerContext { }
Cuando compilas, el generator genera la implementación de AppJsonContext con todo el código de serialización para User y Order. Luego, en lugar de llamar a JsonSerializer.Serialize(user), usas:
string json = JsonSerializer.Serialize(user, AppJsonContext.Default.User);
Sin reflection. El código generado hace exactamente lo mismo que haría System.Text.Json con reflection, pero sin tocar System.Type en ningún momento. Si publicas con NativeAOT:
dotnet publish --runtime linux-x64 -p:PublishAot=true
...el compilador AOT puede analizar el ensamblado entero porque no hay reflection dinámica que se le escape. Sin source generation, la publicación AOT falla o genera advertencias que luego se convierten en errores en runtime.
Incremental Source Generators: el modelo actual
El primer modelo de generators (ISourceGenerator) tenía un problema: se ejecutaba entero en cada compilación, aunque solo hubieras cambiado una línea en un fichero sin relación con el generator. En proyectos grandes, eso encarecía el tiempo de compilación de forma notable.
Desde .NET 6 existe el modelo incremental (IIncrementalGenerator), que funciona con pipelines de datos. Describes qué inputs le interesan al generator, como atributos específicos, archivos adicionales o la configuración del proyecto, y el compilador solo re-ejecuta las partes del pipeline cuyos inputs han cambiado. Si cambias un fichero que el generator no observa, no se regenera nada.
En la práctica: si vas a escribir un generator hoy, usa siempre IIncrementalGenerator. La interfaz ISourceGenerator está marcada como obsoleta y no hay razón para usarla en código nuevo.
Qué puede hacer un generator
Los casos de uso son más amplios de lo que parece a primera vista. Algunos ejemplos concretos que se usan en producción:
- Buscar todos los tipos marcados con un atributo personalizado y generar implementaciones de interfaces para ellos.
- Generar
ToString(),Equals()yGetHashCode()a partir de las propiedades de una clase, sin tener que escribirlos ni mantenerlos a mano. - Leer archivos
.jsono.xmldel proyecto como additional files y generar clases tipadas a partir de su contenido. - Generar un cliente HTTP tipado a partir de una especificación OpenAPI, de forma que si la spec cambia y recompilan, el cliente se actualiza automáticamente.
- Generar registros de DI automáticamente a partir de atributos en las clases de servicio.
El límite es lo que puedas extraer del árbol sintáctico y los archivos adicionales. Que es, en la práctica, bastante.
Generators incluidos en .NET que ya estás usando
Aunque no hayas escrito un generator en tu vida, es probable que ya estés usando varios sin saberlo.
Logging de alto rendimiento
El paquete Microsoft.Extensions.Logging incluye un generator para LoggerMessage. En lugar de llamar a _logger.LogInformation("Usuario {Id} creado", id) cada vez, puedes declarar:
[LoggerMessage(Level = LogLevel.Information, Message = "Usuario {Id} creado")]
partial void LogUsuarioCreado(int id);
El generator genera la implementación. El resultado es logging sin allocations innecesarias, porque el string de formato no se evalúa si el nivel de log está desactivado.
Regex compiladas en tiempo de compilación
Desde C# 10, puedes usar el atributo [GeneratedRegex]:
[GeneratedRegex(@"d+")]
private static partial Regex NumeroRegex();
El generator compila la expresión regular en tiempo de compilación. En runtime, la instancia de Regex ya está lista, sin el coste de parsear el patrón la primera vez que se usa.
Validación de opciones AOT-compatible
Microsoft.Extensions.Options usa source generation para generar la validación de clases de configuración decoradas con [OptionsValidator], de forma que también es compatible con AOT.
partial class y partial method: cómo encaja el código generado
El código que genera un generator se añade al proyecto como archivos .cs adicionales que el compilador incluye en la compilación. Para que el código generado pueda extender tus clases sin conflictos, el patrón habitual es partial class.
Tu código define la clase con la parte de negocio:
public partial class MiModelo
{
public string Nombre { get; set; }
public int Edad { get; set; }
}
El generator añade otro archivo con la parte generada:
public partial class MiModelo
{
public override string ToString() => $"Nombre={Nombre}, Edad={Edad}";
}
El compilador junta ambas partes y produce una sola clase. Tú no ves el archivo generado en el repositorio, porque vive en la carpeta de artefactos de compilación, aunque puedes inspeccionarlo desde el IDE si necesitas depurar lo que genera el generator.
Con partial method el patrón es parecido: declaras la firma en tu código y el generator provee la implementación. Si el generator no la provee, el compilador simplemente omite las llamadas al método.
Cuándo tiene sentido escribir tu propio generator
La pregunta honesta es: ¿merece la pena la complejidad de escribir un generator frente a las alternativas?
Un generator tiene sentido cuando tienes código boilerplate que se repite con la misma estructura para muchos tipos del proyecto, y ese código se podría calcular a partir de la información que ya está en el árbol sintáctico. Si son tres o cuatro clases, lo escribes a mano. Si son cuarenta, y cada vez que añades una tienes que acordarte de añadir el código correspondiente, ahí el generator empieza a tener sentido.
También tiene sentido cuando usas reflection en runtime para algo que se podría determinar en compilación y el perfil de rendimiento de la aplicación indica que eso es un cuello de botella real, o cuando necesitas compatibilidad con AOT.
Antes de llegar a un generator, considera:
- Templates T4: más sencillos, generan archivos que sí se guardan en el repositorio. Útiles para generación puntual.
- Roslyn Analyzers con code fixes: si lo que quieres es detectar patrones y sugerir correcciones, un analyzer es más apropiado que un generator.
- Librerías existentes: hay generators para la mayoría de casos comunes. Antes de escribir el tuyo, busca si alguien ya lo hizo.
Si decides escribir uno, el proyecto es una librería netstandard2.0 que referencia el paquete Microsoft.CodeAnalysis.CSharp. El proyecto que lo consume lo referencia como <ProjectReference> con OutputItemType="Analyzer" y ReferenceOutputAssembly="false".
Depurar un Source Generator
Una de las partes más incómodas cuando empiezas es la depuración. El generator corre dentro del proceso del compilador, no en tu aplicación. Para adjuntar el depurador puedes añadir Debugger.Launch() en el código del generator durante el desarrollo, o usar la opción de launch del IDE que adjunta al proceso de compilación.
También puedes escribir pruebas unitarias para el generator usando el paquete Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing, que permite pasar código fuente como string y verificar qué archivos genera el generator sin necesidad de compilar un proyecto real.
Ver el código generado desde el IDE es tan sencillo como expandir el nodo «Analyzers» en el explorador de soluciones de Visual Studio o Rider. Ahí aparecen los archivos generados y puedes abrirlos para inspeccionar el resultado.
Dónde encajan los Source Generators en el desarrollo moderno de .NET
El impulso principal detrás de los Source Generators, al menos en el marco de .NET, es NativeAOT. Microsoft lleva años trabajando para que .NET sea viable como runtime de funciones serverless de arranque ultra-rápido y aplicaciones de bajo consumo de memoria. NativeAOT lo hace posible, pero exige eliminar la reflection dinámica. Los Source Generators son la respuesta a esa restricción.
Por eso C# y la metaprogramación: de la reflexión a los generators es un recorrido que tiene lógica: la reflection fue la solución durante años, y los generators son el siguiente paso. Y si trabajas con ASP.NET Core y el rendimiento en tiempo de compilación, los generators ya forman parte del stack, aunque no los hayas configurado tú directamente.
No hace falta que escribas generators propios para beneficiarte de ellos. Usar System.Text.Json con source generation, activar el generator de LoggerMessage o usar [GeneratedRegex] son cambios pequeños que ya están disponibles en cualquier proyecto .NET 6 o posterior.
Imagen: Pexels / Muhammed Ensar
