Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 8 de mayo de 2018
ASP.NET Core Hace poco un alumno de mi curso de ASP.NET Core MVC en CampusMVP me preguntaba sobre las posibilidades de utilizar Razor desde una aplicación de consola con el fin de aprovechar dicha sintaxis a la hora de componer emails. Ya en un artículo anterior vimos cómo podíamos conseguirlo desde una aplicación ASP.NET Core MVC, pero en este caso lo que vamos a ver es cómo conseguirlo desde fuera de ASP.NET Core, es decir, desde una aplicación de consola pura sin apenas dependencias a dicho framework.

El problema que tiene intentar usar Razor de esta forma es que estamos muy malacostumbrados ;) ASP.NET Core hace mucho trabajo por nosotros y puede hacernos ver que renderizar una vista es algo trivial, pero no lo es; la vista debe ser parseada para obtener de ella un código C# que más adelante será compilado al vuelo para generar un ensamblado que será anexado a nuestra aplicación de forma dinámica y que será utilizado en cada renderización. Y todo ello, de forma rápida y eficiente en recursos.

La renderización de una vista Razor desde una aplicación de consola “pura” consiste en seguir estos mismos pasos, pero de forma manual. Lo que veremos a lo largo de un par de posts es:
  • Cómo generar código C# parseando una plantilla Razor, es decir, un archivo .cshtml.
  • Cómo compilar el código C# obtenido y generar un ensamblado con Roslyn.
  • Cómo cargar dinámicamente dicho ensamblado en memoria.
  • Cómo ejecutar una vista presente en dicho ensamblado y obtener el resultado.
¡Empecemos! ;)
Nota: el objetivo de estos posts es puramente didáctico, y su única intención es aprender algo sobre las tripas de ASP.NET Core. No nos meteremos en optimizar estas operaciones o introducir mejoras como cacheado o similares, ni en ofrecer una solución funcionalmente completa. Por tanto, lo que veremos aquí no será production ready, pero sí un buen punto de partida para que podáis crear vuestras propias soluciones.

Cómo generar código fuente C# desde una plantilla Razor

Como hemos comentado antes, la primera fase de la ejecución de una plantilla Razor es “compilarla” a C#, es decir, parsearla y obtener de ella el código C# cuya ejecución generará el resultado final, listo para ser utilizado como cuerpo de un mail o para cualquier otro uso.

Afortunadamente, el framework ya nos proporciona herramientas para conseguirlo de forma bastante sencilla en el interior del paquete NuGet Microsoft.AspNetCore.Razor.Language, que debemos incluir nuestro proyecto. De hecho, este será el único paquete relacionado con ASP.NET Core que necesitaremos añadir a la aplicación.

En su interior encontraremos componentes capaces de leer un archivo .cshtml y generar clases C#, que tendrán siempre la siguiente estructura:
public class Template : [BaseClass]
{
    public async override Task ExecuteAsync()
    {
        ...
    }
}
Por defecto el nombre de la clase generada será Template, y la clase base de la que hereda, como veremos más adelante, es un dato que pasaremos al generador de Razor cuando vayamos a utilizarlo.

Ese método ExecuteAsync() que se incluye en las clases generadas es el encargado de escribir, ya en tiempo de ejecución, el resultado de la renderización de la plantilla mediante llamadas a métodos como WriteLiteral() y Write(). Para que quede más claro, a continuación mostramos un ejemplo de contenido de archivo Razor .cshtml:
Multiplication table of @Model

@for (int i = 1; i <= 10; i++)
{
    @:@Model x @i = @(Model*i)
}
Observad que en este caso no es necesario utilizar la clásica directiva @model de Razor en ASP.NET Core, simplemente accedemos a la propiedad Model de la plantilla. Más adelante veremos de dónde sale esta propiedad.
La “compilación” de este archivo Razor generaría una clase con el siguiente método ExecuteAsync():
public async override Task ExecuteAsync()
{
    WriteLiteral("Multiplication table of ");
    Write(Model);
    WriteLiteral("\r\n\r\n");
    for (int i = 1; i <= 10; i++)
    {
        WriteLiteral("    ");
        Write(Model);
        WriteLiteral(" * ");
        Write(i);
        WriteLiteral(" = ");
        Write(Model*i);
        WriteLiteral("\r\n");
    }
}
El método WriteLiteral() se utiliza para escribir constantes de cadena, mientras que Write() se usa para escribir el contenido de expresiones Razor (las que comienzan con “@”). Pero sobre todo, fijaos en un detalle: ninguno de estos métodos está implementado en la clase generada automáticamente.

Por este motivo, a la hora de generar el código C# debemos indicar una clase base que será la encargada de proporcionar la implementación de estos métodos. Esta clase, además, puede incluir otros miembros interesantes, como por ejemplo una propiedad Model para que podamos suministrar a la plantilla información, al igual que hacemos con los view models de MVC.

Veamos una posible implementación de esta clase base, en la que introducimos un parámetro genérico TModel para usarlo como tipo de la propiedad Model:
public class TemplateBase<TModel>
{
    private TextWriter _output = Console.Out;
    protected TModel Model { get; set; }

    protected void WriteLiteral(string literal) => _output.Write(literal);
    protected void Write(object obj) => _output.Write(obj);

    public Task ExecuteAsync(TModel model, TextWriter output = null)
    {
        Model = model;
        _output = output ?? _output;
        return this.ExecuteAsync();
    }
    // Will be overriden by the template class
    public virtual Task ExecuteAsync() => Task.CompletedTask;
}
Fijaos que hacemos que los métodos WriteLiteral() y Write() escriban sus argumentos sobre el TextWriter almacenado en la instancia. Dicho objeto es inicializado con el valor Console.Out, de forma que por defecto el resultado de ejecutar la plantilla será escrito sobre la consola, aunque podemos establecer una salida distinta al invocar a la sobrecarga apropiada de ExecuteAsync(), de forma que sea el proceso que renderice la plantilla el que decida hacia dónde escribir el resultado.

También al invocar el método ExecuteAsync() estableceremos el valor de la propiedad Model que usaremos para enviar información a la plantilla. Dado que la clase generada por Razor heredará TemplateBase<TModel>, podrá utilizar la propiedad Model sin problema.

Con esto, ya tenemos todas las piezas necesarias para utilizar el generador de Razor y obtener el código C# a partir de nuestra plantilla. Vamos a crear ahora una clase que gestione la generación de código desde la plantilla .cshtml, a la que llamaremos RazorGenerator, y cuya implementación veremos a continuación.

Su constructor recibe como argumento la ruta donde se encuentran los archivos Razor a procesar, relativa a la carpeta actual (normalmente, el directorio de bin desde donde se está ejecutando nuestra aplicación). Su único método, GenerateCode(), configura Razor para que genere las clases tal y como nos interesa, obtiene la plantilla solicitada y genera su código C# utilizando el motor de plantillas, retornándolo en forma de cadena de caracteres:
public class RazorGenerator
{
    private readonly string _templateRoot;

    public RazorGenerator(string templateRoot)
    {
        _templateRoot = templateRoot;
    }
    public string GenerateCode<TModel>(string templateName)
    {
        var engine = RazorEngine.Create(b =>
        {
            // Define the namespace for the generated class
            b.SetNamespace($"{typeof(TemplateBase<>).Namespace}.Templates");
            b.ConfigureClass((razorCode, outputClass) =>
            {
                // Define the generated class name
                // TODO: Sanitize & ensure that templateName is a valid C# class name
                outputClass.ClassName = templateName;
                // Define its base type
                outputClass.BaseType = 
                    $"{typeof(TemplateBase<>).Namespace}."
                    + nameof(TemplateBase<TModel>) 
                    + $"<{typeof(TModel).FullName}>";
            });
            b.Build();
        });

        var project = RazorProject.Create(
            Path.Combine(Directory.GetCurrentDirectory(), _templateRoot)
        );

        var templateEngine = new RazorTemplateEngine(engine, project);
        templateEngine.Options.ImportsFileName = "_ViewImports.cshtml";

        var item = project.GetItem(templateName + ".cshtml");
        var code = templateEngine.GenerateCode(item);
        return code.GeneratedCode;
    }
}
// TODO: por simplificar, en este momento asumimos que el nombre de la plantilla siempre será un identificador de clase válido en C#, pero deberíamos comprobarlo y sanearlo antes de continuar.
Para utilizar el código anterior e ir viendo resultados, podríamos hacer algo parecido a lo que mostramos a continuación, donde compilamos la plantilla Templates/Hello.cshtml y mostramos el resultado por consola:
var generator = new RazorGenerator("Templates");    // Folder "Templates"
var code = generator.GenerateCode<string>("Hello"); // Template "Hello.cshtml"
Console.WriteLine(code);
Explorador de solucionesOjo, porque la ubicación de la plantilla es por defecto relativa al binario de la aplicación, por lo que lo más cómodo es acceder a las propiedades del archivo en el explorador de soluciones de Visual Studio y establecer la propiedad “Copy to Output Directory” a “Copy if newer”.

La plantilla Templates/Hello.cshtml podría tener un contenido como el siguiente:
@* Templates/Hello.cshtml *@
Hello, @Model
De esta forma, por consola obtendríamos el siguiente resultado: un flamante código C# listo para ser compilado de forma dinámica más adelante (lo he limpiado un poco por claridad):
namespace RazorRenderer
{
    public class Test : TemplateBase<string>
    {
        public async override Task ExecuteAsync()
        {
            WriteLiteral("Hello, ");
            Write(Model);
        }
    }
}
¿Mola, eh?

Bueno, pues de momento lo vamos a dejar aquí. En el próximo post veremos cómo compilar este código y ejecutarlo para renderizar nuestra plantilla :)

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!