Quantcast
Channel: Planeta Código
Viewing all articles
Browse latest Browse all 2725

Variable not found: C# source generators: un ejemplo sencillo, paso a paso

$
0
0
.NET Core

Como decíamos hace unos días, los generadores de código C# nos brindan la posibilidad de crear al vuelo código C# e incluirlo en nuestros proyectos en tiempo de compilación.

Por no alargar demasiado el post, vimos un sencillísimo ejemplo de implementación, pero ahora vamos a crear algo más complejo que podría ayudarnos a solucionar un problema que tendría difícil solución de no contar con esta característica del compilador.

1. Definición de objetivos

El reto al que vamos a enfrentarnos ya lo expusimos en el post anterior como un caso de uso simple de los generadores de código, así que vamos a reproducir la descripción del escenario.

Imaginemos que en nuestra aplicación tenemos clases que representan operadores matemáticos como SumOperator, MultiplyOperator, DivideOperator, SubtractOperator. Imaginad también que nos interesa tener un tipo enum Operators donde aparezca un miembro por cada operador disponible, algo como:

public enum Operators
{
Sum,
Multiply,
Divide,
Subtract
}

El problema que tiene enfocar esto de forma manual es que resultaría sencillo implementar una nueva clase operador y olvidar crear su correspondiente entrada en la enumeración Operators. Aquí es donde vienen al rescate los generadores de código :)

Lo que implementaremos hoy es un generador de código C# que creará la enumeración por nosotros en tiempo de compilación, manteniéndola sincronizada en todo momento con las clases que tengamos definidas en el proyecto. Para ello, crearemos un generador llamado OperatorsEnumGenerator que:

  • En la fase de análisis de código recopilará las clases del proyecto a compilar cuyo nombre finalice por Operator.
  • En la fase de generación de código creará el enum con los miembros registrados anteriormente.

¡Vamos allá!

2. Creación del proyecto del generador

El proyecto del generador de código C# es una simple biblioteca de clases .NET Standard 2.0 en la que instalamos los siguientes paquetes NuGet:

  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.Analyzers

Una vez preparado el entorno, añadamos el andamiaje de nuestro generador introduciendo la clase OperatorsEnumGenerator que, como sabemos, debe implementar la interfaz ISourceGenerator y estar decorada con el atributo [Generator:

[Generator]
public class OperatorsEnumGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// TODO
}

public void Execute(GeneratorExecutionContext context)
{
// TODO
}
}

En el método Initialize() es donde configuraremos los analizadores de código del proyecto que está siendo compilado, que se ejecutarán durante la primera fase. En este caso, crearemos un analizador que simplemente recopilará los nombres de las clases del proyecto cuyo nombre acabe en Operator.

Luego, en Execute(), generaremos el código de la enumeración Operators basándonos en la colección de clases que recopilamos anteriormente.

2.1. Obteniendo los nombres de las clases operador

Para obtener el nombre de las clases que el compilador va recorriendo durante la compilación, debemos crear una implementación de ISyntaxReceiver. El método OnVisitSyntaxNode() es invocado cada vez que el compilador encuentra un nodo sintáctico (clases, propiedades, métodos, etc.), por lo que podemos aprovechar ese momento para almacenar los nombres de las clases que nos interesen.

Como podéis ver, la implementación es trivial; cuando recibimos un nodo de tipo ClassDeclarationSyntax y su nombre acaba en "Operator", recortamos este sufijo y lo añadimos a una colección en memoria:

public class OperatorTypesCollector : ISyntaxReceiver
{
public List<string> OperatorTypes { get; } = new ();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax c
&& c.Identifier.Text.EndsWith("Operator"))
{
var name = c.Identifier.Text;
OperatorTypes.Add(
name.Substring(0, name.Length - "Operator".Length)
);
}
}
}

Una vez creado, debemos registrar nuestro recolector en el método Initialize() del generador, de la siguiente manera. Como se puede intuir, lo que hacemos es registrar en el contexto del generador, mediante el método RegisterForSyntaxNotifications(), el delegado que se encargará de crear la clase que será notificada al visitar los nodos sintácticos:

[Generator]
public class OperatorsEnumGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(
() => new OperatorTypesCollector()
);
}

public void Execute(GeneratorExecutionContext context)
{
// TODO
}
}

2.2. Generando el código

Durante la primera pasada del compilador, nuestra clase OperatorTypesCollector se encargará de almacenar los nombres de los operadores que se han ido encontrando. Ya estamos en disposición de implementar el método Execute(), que es donde generaremos el código partiendo de esta información.

De nuevo, la cosa es bastante sencilla. Echad un vistazo al código y lo comentamos justo después:

public void Execute(GeneratorExecutionContext context)
{
var collector = (OperatorTypesCollector)context.SyntaxReceiver;
var names = string.Join(", ", collector.OperatorTypes);

var code = @$"
namespace Shared
{{
public enum Operators
{{
{names}
}}
}}";

context.AddSource("Operators.cs", SourceText.From(code, Encoding.UTF8));
}

Como podéis ver, lo único que hacemos es obtener una referencia hacia la instancia de nuestro ISyntaxReceiver para obtener de la lista de nombres que ha recopilado. El resto sólo consiste en construir el código fuente C# que vamos a emitir y añadirlo al proyecto con un nombre de archivo (que debe ser único).

De esta forma, si incluimos nuestro generador en un proyecto que contenga las clases SumOperator y MultiplyOperator, se añadirá al mismo el siguiente archivo de código fuente:

namespace Shared
{
public enum Operators
{
Sum, Multiply
}
}

3. Utilizando el generador desde otro proyecto

Como hemos comentado anteriormente, los generadores pueden ser añadidos fácilmente a los proyectos en los que queremos utilizarlos. La forma más sencilla de hacerlo es, sin duda, mediante una referencia directa entre proyectos.

En la captura de pantalla lateral podemos observar una solución con un proyecto llamado Generators, donde hemos implementado nuestro generador, y otro proyecto llamado Calculator desde donde vamos a consumirlo. Lo único a tener en cuenta es que este proyecto debe ser .NET 5 (aunque también puede ser .NET Core 3.1, siempre que se establezca la versión del lenguaje a preview).

Desde Calculator hemos añadido una referencia al proyecto de los generadores, indicando que se trata de un analizador de código en el archivo de proyecto .csproj:

<ItemGroup>
<ProjectReference
Include="..\Generators\Generators.csproj"
OutputItemType="Analyzer" />
</ItemGroup>

Ahora, para probar el generador basta con introducir el siguiente código en el archivo Program.cs del proyecto principal:

class Program
{
static void Main(string[] args)
{
Console.WriteLine("Operators:");
foreach (var op in Enum.GetNames(typeof(Shared.Operators)))
{
Console.WriteLine(op);
}
}
}

public class SumOperator { }
public class MultiplyOperator { }
// TODO: Implementar otros operadores

Como habéis podido comprobar, la aplicación define un par de clases que siguen la convención de nombrado para operadores (SumOperator y MultiplyOperator), y muestra por consola los miembros del enumerado Shared.Operators que es insertado en el proyecto de forma automática por el generador de código.

Y si ejecutamos la aplicación, el resultado será el esperado:

Operators:
Sum
Multiply

¡Y esto es todo, ya tenemos nuestro generador en marcha! Recapitulando un poco, el proceso ha consistido en:

  • Crear un proyecto de generadores de código, con:
    • Target .NET Standard 2.0.
    • Referencias a Microsoft.CodeAnalysis.CSharp y Microsoft.CodeAnalysis.Analyzers.
  • En dicho proyecto, añadir la clase del generador, que implementa ISourceGenerator y está decorada con [Generator]. En ella:
    • Registrar un ISyntaxReceiver para recopilar información del proyecto en su método Initialize().
    • Usar en el método Execute() la información recopilada para componer el contenido de los archivos de código fuente a generar, y añadirlos finalmente al proyecto.

Espero que este ejemplo os haya sido de utilidad para comprender cómo funcionan los generadores. Y si es así, seguro que ya estaréis pensando en otros escenarios para sacar partido a este potente mecanismo, que desde luego va a dar mucho juego en nuestros proyectos.

Publicado en Variable not found.


Viewing all articles
Browse latest Browse all 2725

Trending Articles


Gwapo Quotes : Babaero Quotes


Girasoles para colorear


tagalog love Quotes – Tiwala Quotes


Long Distance Relationship Tagalog Love Quotes


Ligaw Quotes – Courting Quotes – Sweet Tagalog Quotes


INUMAN QUOTES


Hugot Lines 2020 Patama : ML Edition


Re:Mutton Pies (lleechef)


Vimeo 10.7.0 by Vimeo.com, Inc.


Vimeo 10.7.1 by Vimeo.com, Inc.


FORECLOSURE OF REAL ESTATE MORTGAGE


Sapos para colorear


Love Quotes Tagalog


Two timer Sad tagalog Love quotes


Inggit Quotes and Kabit Quotes – Tagalog Patama Quotes


5 Uri ng Pambobola


Best Love Life Quotes Collection


RE: Mutton Pies (frankie241)


EASY COME, EASY GO


Pokemon para colorear