Seguramente muchos coincidiremos en que una de las novedades más interesantes de la última versión del compilador de C# es lo que oficialmente han denominado C# Source Generators, o generadores de código fuente de C#.
Muy resumidamente, esta característica añade un nuevo paso en la compilación en el cual los desarrolladores podemos introducir componentes propios (generadores) que inspeccionen el código de la aplicación que está siendo compilada y generen nuevos archivos, que a su vez pueden ser compilados e incluidos en los ensamblados resultantes. Su objetivo, tal y como se declara en su documento de diseño, es posibilitar la metaprogramación en tiempo de compilación.
Veámoslo con un ejemplo donde, además de explicarlo mejor, se puede mostrar su utilidad. Imaginad que en nuestra aplicación tenemos clases que representan operadores matemáticos como SumOperator
, MultiplyOperator
, DivideOperator
, SubtractOperator
, y todos ellos heredan de una clase base Operator
. Imaginad también que nos interesa tener un tipo enumerado enum Operators
donde aparezca un miembro por cada operador disponible, algo como:
public enum Operators
{
Sum,
Multiply,
Divide,
Subtract
}
Muy probablemente os habéis encontrado alguna vez con un escenario similar y habéis sufrido la dificultad de mantener sincronizada la enumeración con las clases que heredan de Operator
: cada vez que aparezca un operador nuevo e implementemos la clase operador que lo representa, tendremos que acordarnos de ir a Operators
y añadir el miembro.
Pues bien, aunque simple, esto sería un caso de uso bastante claro para los generadores de código fuente de C#. Gracias a ellos, podríamos crear un componente generador que examine nuestro código en busca de herederos de Operator
y genere al vuelo, siempre en tiempo de compilación, un archivo de código con la enumeración Operators
.
A todos los efectos, es como si esa enumeración la hubiéramos escrito a mano, porque podremos usarla con normalidad, aparecerá en intellisense, etc., pero la diferencia es que será generada cada vez que compilemos el proyecto, asegurando así que siempre será correcta y completa.
Algunos escenarios de uso
Seguro que si lo pensáis un rato podéis encontrar utilidades para esta nueva herramienta. De hecho, muchas de las cosas que a día de hoy tenemos que hacer necesariamente utilizando reflection podríamos llevarlas a un source generator y así mover a tiempo de compilación tareas que tradicionalmente serían postergadas hasta el tiempo de ejecución.
Por ejemplo, durante su arranque, las aplicaciones ASP.NET Core deben autoexaminarse para construir su application model, que contiene toda la información sobre sus controladores, acciones, parámetros, rutas, etc. Esto, que se realiza mediante reflexión, podría ser implementado (y lo será de hecho) como source generators, de forma que quedará resuelto en tiempo de compilación.
Otro ejemplo lo podéis ver claro si pensáis en el mapeo de objetos, como los que hacemos para mover datos de entidades EF a DTOs, o entre DTOs y View models. Hacerlo a mano es una tarea ardua y muy propensa a errores, por lo que solemos utilizar herramientas automáticas como Automapper que, a pesar de sus optimizaciones, deben analizar los mapeos en tiempo de ejecución para poder emitir código de mapeo optimizado. En este caso, de nuevo podríamos llevar a tiempo de compilación la generación de clases que mapeen objetos de forma directa.
¿Y los serializadores o deserializadores de objetos? Pues ahí tenemos otro claro escenario de uso de los generadores de código, pues los componentes que realizan estas tareas se basan tradicionalmente en reflexión, con la penalización de rendimiento que ello supone. Con esta nueva herramienta, podrá generarse en tiempo de compilación código específico de serialización/deserialización para cada clase que nos interese.
Un ejemplo más: si usáis Xamarin, WPF u otros frameworks basados en bindings de propiedades que requieren notificación... ¿a que os aburre crear los dichosos observables? Pues de nuevo, un generador de código podría recorrer los campos que cumplan una serie de condiciones (p.e. que estén decoradas con un atributo determinado) y generar por nosotros las típicas propiedades con la lógica de notificación tan habituales.
Por último, teniendo en cuenta que los generadores de código están escritos en C#, podemos utilizar toda la potencia del lenguaje y de .NET Framework para hacer casi cualquier cosa. Por ejemplo, podríamos generar clases de código que para llevar a cabo su tarea usen archivos externos, conecten con bases de datos, servicios externos o lo que nos interese (teniendo siempre en cuenta que esto se realizará en tiempo de compilación, claro).
¿Cómo funciona esto?
Los generadores de código fuente son componentes implementados en ensamblados .NET Standard 2.0. Para utilizar un generador desde un proyecto, basta con referenciar el ensamblado donde esté definido (o bien su paquete NuGet, si se distribuye de esta forma).
Una vez introducidos en un proyecto, los generadores podrán realizar básicamente dos tareas durante la compilación: examinar el proyecto que está siendo compilado y añadir nuevos archivos de código fuente.
- Pueden examinar el código fuente del proyecto que está siendo compilado a través de objetos, construidos por el compilador, que representan cada uno de los nodos sintácticos que lo componen (archivos, clases, miembros, etc.). Esto es bastante similar a los analizadores de código que ya llevan tiempo entre nosotros.
- Adicionalmente, los generadores pueden añadir al proyecto archivos de código fuente. Ojo porque esto es importante: un generador de código no puede modificar el fuente existe. Por tanto, no podemos modificar o eliminar clases, miembros o cualquier otro elemento. Pero nada impide que añadamos nuevas clases, o extendamos clases parciales existentes aportándoles miembros, atributos o cualquier otro aspecto permitido en este tipo de artefactos.
El siguiente diagrama, que he tomado prestado del post de Phillip Carter Introducing C# Source Generators, resume cómo el proceso de generación está integrado como un paso más dentro de la compilación:
¿Y qué pinta tiene a nivel de código?
Internamente, un generador de código fuente es simplemente una clase decorada con el atributo [Generator]
y que implementa la interfaz ISourceGenerator
, definida en Microsoft.CodeAnalysis
de la siguiente forma:
public interface ISourceGenerator
{
void Initialize(GeneratorInitializationContext context);
void Execute(GeneratorExecutionContext context);
}
El método Initialize()
nos permitirá configurar los objetos que analizarán el código del proyecto que está siendo compilado, mientras que en Execute()
será donde realmente generaremos el código y lo añadiremos al proyecto.
El siguiente ejemplo muestra un generador que añadirá a los proyectos que lo utilicen el código de una clase estática con un método para mostrar un mensaje de saludo por consola:
[Generator]
public class HelloWorldGenerator: ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// Nothing to do
}
public void Execute(GeneratorExecutionContext context)
{
var code = @"
namespace Utils
{
public static class HelloWorld
{
public static void Show()
{
System.Console.WriteLine(""Hello world!"");
}
}
}";
context.AddSource("Generated", code);
}
}
De esta forma, cualquier proyecto que referencie al generador podrá utilizar una expresión como Utils.HelloWorld.Show()
para mostrar el mensaje, dado que el código fuente insertado por el generador estará disponible a todos los efectos desde el proyecto.
Fijaos que en este caso no estamos analizando el código del proyecto ni nada parecido; por simplificar, generamos un código C# estático, lo cual tampoco resulta demasiado interesante, aunque os puede dar una idea de su potencial porque ese código podría incluir contenido calculado de forma dinámica (por ejemplo, obteniendo datos de un archivo externo).
Para evitar que este post sea demasiado extenso, en próximas entregas implementaremos paso a paso un generador algo más complejo, y seguro os ayudarán a entender mejor su funcionamiento y visualizar el alcance que esta nueva característica pone en nuestras manos.
Mientras, os dejo unos enlaces por si queréis ir ampliando información:
Enlaces:
- Introducing C# Source Generators
- New C# Source Generator Samples
- Source generators design document
- Source generators examples
- Source generators cookbok
Publicado en Variable not found.
Publicado por José M. Aguilar a las 8:05 a. m.
Etiquetas: .net, c#, generadores, metaprogramación, novedades
3 Comentarios:
Gracias por tu comentario :)
Ya sabes, esto es como todo: una herramienta muy potente, pero que mal utilizada puede llevar a quebraderos de cabeza. Pero bueno, mirando el lado positivo, creo que abre posibilidades muy interesantes que hasta ahora no teníamos disponibles (o al menos, no de forma sencilla).
Saludos!
Me pregunto si el cliente de GRPC que genera clases a partir de los ficheros .proto utilizará esta caracteristica...
Más allá de eso, se me ocurren bastantes utilidades muy interesantes, imagina por ejemplo un RPG Maker, que utilizando estas caracteristica, cuando desde el editor añadas una propiedad nueva a un personaje (por ejemplo, la vida), genere automaticamente una clase que represente al personaje y tengas acceso a su vida, propiedad definida directamente en el editor.
Hola!
Pues en algún sitio me ha parecido leer que estaban en ello, pero no recuerdo exactamente dónde y no he podido volver a encontrarlo. Pero vaya, es un escenario donde los generadores encajarían bien.
Efectivamente, la característica tiene muchos escenarios de aplicación. Algunos los usaremos "de serie" con frameworks como ASP.NET Core, Blazor, Xamarin, etc., pero basta pensar un poco para encontrar nuevas posibilidades, como la que comentas.
Gracias por aportar!
Enviar un nuevo comentario