Generadores de origen

En este artículo se ofrece información general sobre los generadores de código fuente que se incluye como parte del SDK de .NET Compiler Platform ("Roslyn"). Los generadores de código fuente permiten a los desarrolladores de C# inspeccionar el código de usuario a medida que se compila. El generador puede crear nuevos archivos de código fuente de C# sobre la marcha que se agregan a la compilación del usuario. De este modo, tiene código que se ejecuta durante la compilación. Además, inspecciona el programa para generar archivos de código fuente adicionales que se compilan junto con el resto del código.

Un generador de código fuente es un nuevo tipo de componente que los desarrolladores de C# pueden escribir y que le permite hacer dos cosas principales:

  1. Recuperar un objeto de compilación que representa todo el código de usuario que se está compilando. Este objeto se puede inspeccionar, y puede escribir código que funcione con la sintaxis y los modelos semánticos del código que se compila, al igual que con los analizadores de hoy en día.

  2. Genere archivos de código fuente de C# que se puedan agregar a un objeto de compilación durante la compilación. En otras palabras, puede proporcionar código fuente adicional como una entrada en una compilación mientras se compila el código.

Cuando se combinan, estas dos cosas son las que hacen que los generadores de código fuente sean tan útiles. Puede inspeccionar el código de usuario con todos los metadatos enriquecidos que el compilador crea durante la compilación. Después, el generador vuelve a emitir código de C# en la misma compilación que se basa en los datos que ha analizado. Si está familiarizado con los analizadores de Roslyn, puede pensar en los generadores de código fuente como analizadores que pueden emitir código fuente de C#.

Los generadores de código fuente se ejecutan como una fase de compilación que se visualiza a continuación:

Gráfico que describe las distintas partes de la generación de código fuente

Un generador de código fuente es un ensamblado de .NET Standard 2.0 que el compilador carga junto con cualquier analizador. Se puede usar en entornos en los que se pueden cargar y ejecutar los componentes de .NET Standard.

Importante

Actualmente, solo se pueden usar ensamblados de .NET Standard 2.0 como generadores de origen.

Escenarios frecuentes

Existen tres enfoques generales para inspeccionar el código de usuario y generar información o código basado en ese análisis que usan las tecnologías de hoy en día:

  • Reflexión en tiempo de ejecución.
  • Tareas de MSBuild pendientes.
  • Tejido de lenguaje intermedio (IL) (no se describe en este artículo).

Los generadores de código fuente pueden mejorar cada enfoque.

Reflexión en tiempo de ejecución

La reflexión en tiempo de ejecución es una tecnología eficaz que se agregó a .NET hace mucho tiempo. Existen incontables escenarios para usarla. Un escenario común es realizar algún análisis del código de usuario cuando se inicia una aplicación y usar los datos para generar elementos.

Por ejemplo, ASP.NET Core usa la reflexión cuando el servicio web se ejecuta por primera vez para detectar las construcciones que ha definido para que pueda "conectar" elementos, como controladores y Razor Pages. Aunque este escenario le permite escribir código sencillo con abstracciones eficaces, viene acompañado de una penalización de rendimiento en tiempo de ejecución: cuando el servicio web o la aplicación se inician por primera vez, no pueden aceptar ninguna solicitud hasta que todo el código de reflexión en tiempo de ejecución que descubre información sobre el código termine de ejecutarse. Aunque esta penalización de rendimiento no es enorme, es un costo fijo que no puede mejorar por sí mismo en su propia aplicación.

Con un generador de código fuente, la fase de detección del controlador de inicio podría producirse en tiempo de compilación. Un generador puede analizar el código fuente y emitir el código que necesita para "conectar" la aplicación. El uso de generadores de código fuente podría dar lugar a tiempos de inicio más rápidos, ya que una acción que está teniendo lugar en tiempo de ejecución podría insertarse en tiempo de compilación.

Tareas de MSBuild pendientes

Los generadores de origen pueden mejorar el rendimiento de formas que no se limitan a la reflexión en tiempo de ejecución para descubrir tipos también. Algunos escenarios implican llamar a la tarea de C# de MSBuild (denominada CSC) varias veces para que puedan inspeccionar los datos desde una compilación. Como puede imaginar, llamar al compilador más de una vez afecta al tiempo total que se tarda en compilar la aplicación. Estamos investigando cómo se pueden usar los generadores de código fuente para obviar la necesidad de realizar tareas de MSBuild como esta, ya que los generadores de código fuente no solo ofrecen algunas ventajas de rendimiento, sino que también permiten que las herramientas funcionen en el nivel de abstracción correcto.

Otra funcionalidad que los generadores de código fuente pueden ofrecer es obviar el uso de algunas API "fuertemente tipadas"; por ejemplo, el modo en que funciona el enrutamiento de ASP.NET Core entre controladores y Razor Pages. Con un generador de código fuente, el enrutamiento puede estar fuertemente tipado con la generación de las cadenas necesarias como un detalle en tiempo de compilación. Esto reduciría la cantidad de veces que un literal de cadena mal escrito genera una solicitud que no llega al controlador correcto.

Introducción a los generadores de código fuente

En esta guía, explorará la creación de un generador de código fuente mediante la API ISourceGenerator.

  1. Creación de una aplicación de consola .NET Este ejemplo usa .NET 6.

  2. Reemplace la clase Program por el siguiente código. El código siguiente no usa instrucciones de nivel superior. La forma clásica es necesaria porque este primer generador de código fuente escribe un método parcial en esa clase Program:

    namespace ConsoleApp;
    
    partial class Program
    {
        static void Main(string[] args)
        {
            HelloFrom("Generated Code");
        }
    
        static partial void HelloFrom(string name);
    }
    

    Nota

    Puede ejecutar este ejemplo tal y como está, pero todavía no ocurrirá nada.

  3. A continuación, crearemos un proyecto de generador de origen que implementará el método partial void HelloFrom equivalente.

  4. Cree un proyecto de biblioteca .NET Standard que se dirija al moniker de la plataforma de destino (TFM) netstandard2.0. Agregue los paquetes NuGet Microsoft.CodeAnalysis.Analyzers y Microsoft.CodeAnalysis.CSharp:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
      </ItemGroup>
    
    </Project>
    

    Sugerencia

    El proyecto del generador de origen debe tener como destino el TFM netstandard2.0; de lo contrario, no funcionará.

  5. Cree un archivo de C# denominado HelloSourceGenerator.cs que especifique su propio generador de origen de la siguiente manera:

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Code generation goes here
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    Un generador de origen debe implementar la interfaz Microsoft.CodeAnalysis.ISourceGenerator y tener Microsoft.CodeAnalysis.GeneratorAttribute. No todos los generadores de código fuente requieren inicialización, y es el caso de esta implementación de ejemplo, donde el objeto ISourceGenerator.Initialize está vacío.

  6. Reemplace el contenido del método ISourceGenerator.Execute, con la siguiente implementación:

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Find the main method
                var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);
    
                // Build up the source code
                string source = $@"// <auto-generated/>
    using System;
    
    namespace {mainMethod.ContainingNamespace.ToDisplayString()}
    {{
        public static partial class {mainMethod.ContainingType.Name}
        {{
            static partial void HelloFrom(string name) =>
                Console.WriteLine($""Generator says: Hi from '{{name}}'"");
        }}
    }}
    ";
                var typeName = mainMethod.ContainingType.Name;
    
                // Add the source code to the compilation
                context.AddSource($"{typeName}.g.cs", source);
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    A partir del objeto context, se puede acceder al punto de entrada de las compilaciones o al método Main. La instancia mainMethod es IMethodSymbol, y representa un método o un símbolo de tipo método (incluido el constructor, destructor, operador o descriptor de acceso de propiedad/evento). El método Microsoft.CodeAnalysis.Compilation.GetEntryPoint devuelve el objeto IMethodSymbol del punto de entrada del programa. Otros métodos permiten buscar cualquier símbolo del método en un proyecto. A partir de este objeto, podemos razonar sobre el espacio de nombres que lo contiene (si hay alguno) y el tipo. El objeto source de este ejemplo es una cadena interpolada que crea plantillas para el código fuente que se va a generar, donde los marcadores interpolados se rellenan con el espacio de nombres que los contiene y la información de tipo. source se agrega a context con un nombre de sugerencia. En este ejemplo, el generador crea un nuevo archivo de código fuente generado que contiene una implementación del método partial en la aplicación de consola. Puede escribir generadores de código fuente para agregar cualquier código fuente que desee.

    Sugerencia

    El parámetro hintName del método GeneratorExecutionContext.AddSource puede ser cualquier nombre único. Es habitual proporcionar una extensión de archivo de C# explícita, como ".g.cs" o ".generated.cs" para el nombre. El nombre de archivo ayuda a identificar el archivo como generado en el origen.

  7. Ahora tenemos un generador en funcionamiento, pero es necesario conectarlo a nuestra aplicación de consola. Edite el proyecto de aplicación de consola original y agregue lo siguiente, reemplazando la ruta de acceso del proyecto por la del proyecto de .NET Standard que creó anteriormente:

    <!-- Add this as a new ItemGroup, replacing paths and names appropriately -->
    <ItemGroup>
        <ProjectReference Include="..\PathTo\SourceGenerator.csproj"
                          OutputItemType="Analyzer"
                          ReferenceOutputAssembly="false" />
    </ItemGroup>
    

    La nueva referencia no es una referencia de proyecto tradicional y se debe editar manualmente para incluir los atributos OutputItemType y ReferenceOutputAssembly. Para más información sobre los atributos OutputItemType y ReferenceOutputAssembly de ProjectReference, consulte Elementos comunes de proyectos de MSBuild: ProjectReference.

  8. Ahora, al ejecutar la aplicación de consola, debería ver que el código generado se ejecuta e imprime en la pantalla. La propia aplicación de consola no implementa el método HelloFrom, sino que es el código fuente generado durante la compilación desde el proyecto del generador de código fuente. El texto siguiente es un ejemplo de resultado de la aplicación:

    Generator says: Hi from 'Generated Code'
    

    Nota

    Es posible que deba reiniciar Visual Studio para ver IntelliSense y deshacerse de los errores a medida que se mejora activamente la experiencia con las herramientas.

  9. Si usa Visual Studio, puede ver los archivos generados del origen. En la ventana Explorador de soluciones, expanda Dependencias>Analizadores>SourceGenerator>SourceGenerator.HelloSourceGenerator y haga doble clic en el archivo Program.g.cs.

    Visual Studio: archivos generados del origen en el Explorador de soluciones

    Al abrir este archivo generado, Visual Studio indicará que el archivo se genera automáticamente y que no se puede editar.

    Visual Studio: archivo Program.g.cs generado automáticamente

  10. También puede establecer propiedades de compilación para guardar el archivo generado y controlar dónde se almacenan los archivos generados. En el archivo de proyecto de la aplicación de consola, agregue el elemento <EmitCompilerGeneratedFiles> a un objeto <PropertyGroup> y establezca su valor en true. Vuelva a compilar el proyecto. Ahora, los archivos generados se crean en obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator. Los componentes de la ruta de acceso se asignan a la configuración de compilación, la plataforma de destino, el nombre del proyecto del generador de código fuente y el nombre de tipo completo del generador. Puede elegir una carpeta de salida más conveniente agregando el elemento <CompilerGeneratedFilesOutputPath> al archivo de proyecto de la aplicación.

Pasos siguientes

En la guía paso a paso de los generadores de código fuente se abordan algunos de estos ejemplos con algunos enfoques recomendados para resolverlos. Además, tenemos un conjunto de ejemplos disponibles en GitHub que puede probar por su cuenta.

Puede aprender más sobre los generadores de código fuente en estos artículos: