Diciembre de 2016
Volumen 31, número 13
Roslyn: generar JavaScript con Roslyn y plantillas T4
Por Nick Harrison | Diciembre de 2016
El otro día, mi hija me contó un chiste sobre una conversación entre un smartphone y un teléfono móvil de gama media. Era algo así: ¿Qué le dice un smartphone a un teléfono móvil de gama media? "Vengo del futuro, ¿me entiendes?" A veces, se tiene esa sensación al aprender algo nuevo y de vanguardia. Roslyn viene del futuro y, al principio, puede resultar difícil de entender.
En este artículo, hablaré de Roslyn de manera que quizás no le preste toda la atención que se merece. Me centraré en el uso de Roslyn como origen de metadatos para generar JavaScript con T4. Para ello, se usa Workspace API, parte de Syntax API, Symbol API y una plantilla de entorno en tiempo de ejecución del motor T4. El código JavaScript real que se genera es menos importante que comprender los procesos usados para recopilar los metadatos.
Dado que Roslyn también ofrece buenas opciones para generar código, podría pensar que estas dos tecnologías podrían chocar y no funcionar bien juntas. A menudo, las tecnologías chocan cuando sus espacios aislados se solapan, pero estas dos tecnologías pueden funcionar bastante bien juntas.
Espere, ¿qué es T4?
Si no conoce T4, encontrará toda la información que necesita en un e-book de 2015 de la serie Syncfusion Succinctly: "T4 Succinctly" (bit.ly/2cOtWuN).
De momento, lo que necesita saber es que T4 es un kit de herramientas de transformación de texto basado en plantillas de Microsoft. Se suministran metadatos a la plantilla y el texto se convierte en el código que quiere. De hecho, no está limitado al código. Puede generar cualquier tipo de texto, pero el código fuente es la salida más común. Puede generar HTML, SQL, documentación de texto, Visual Basic .NET, C# o cualquier otra salida basada en texto.
Observe la Figura 1. Muestra un sencillo programa de aplicación de consola. En Visual Studio, agregué una nueva plantilla de texto de entorno en tiempo de ejecución denominada AngularResourceService.tt. El código de la plantilla genera automáticamente un código C# que implementará la plantilla en tiempo de ejecución. Podrá verlo desde la ventana de la consola.
Figura 1 Uso de T4 para la generación de código en tiempo de diseño
En este artículo, le enseñaré cómo usar Roslyn para recopilar metadatos de un proyecto de Web API para suministrarlos a T4 a fin de generar una clase JavaScript y, después, usar Roslyn para volver a agregar ese código JavaScript a la solución.
Conceptualmente, el flujo del proceso tendrá un aspecto similar al de la Figura 2.
Figura 2 Flujo de proceso de T4
Roslyn es la fuente de T4
La generación de código es un proceso que se alimenta de metadatos. Para describir el código que se quiere generar, se necesitan metadatos. La reflexión, el modelo de código y el diccionario de datos son fuentes comunes de metadatos disponibles de inmediato. Roslyn puede proporcionar todos los metadatos que recibiría de la reflexión o del modelo de datos, pero sin algunos de los problemas que provocan estos otros enfoques.
En este artículo, usaré Roslyn para buscar clases derivadas de ApiController. Usaré la plantilla T4 para crear una clase JavaScript para cada controlador, y exponer un método para cada acción y una propiedad para cada propiedad del ViewModel asociado con el controlador. El resultado será similar al código de la Figura 3.
Figura 3 Resultado de ejecutar el código
var app = angular.module("challenge", [ "ngResource"]);
app.factory(ActivitiesResource , function ($resource) {
return $resource(
'http://localhost:53595//Activities',{Activities : '@Activities'},{
Id : "",
ActivityCode : "",
ProjectId : "",
StartDate : "",
EndDate : "",
, get: {
method: "GET"
}
, put: {
method: "PUT"
}
, post: {
method: "POST"
}
, delete: {
method: "DELETE"
}
});
});
Recopilación de metadatos
Para empezar a recopilar metadatos, creo un proyecto de aplicación de consola nuevo en Visual Studio 2015. En este proyecto, tengo una clase dedicada a recopilar metadatos con Roslyn, además de una plantilla T4. Será una plantilla de entorno de tiempo de ejecución que generará código JavaScript basado en los metadatos recopilados.
Una vez creado el proyecto, se emiten los comandos siguientes desde la Consola del Administrador de paquetes:
Install-Package Microsoft.CodeAnalysis.CSharp.Workspaces
Esto garantiza que se usará el código de Roslyn más reciente para el compilador CSharp y los servicios relacionados.
Agrego el código para los distintos métodos en una clase nueva denominada RoslynDataProvider. Me referiré a esta clase a lo largo del artículo y será una referencia útil a la hora de recopilar metadatos con Roslyn.
Uso MSBuildWorksspace para obtener un área de trabajo que proporcionará todo el contexto necesario para la compilación. Una vez tengo la solución, puedo desplazarme fácilmente por los proyectos en busca del proyecto WebApi:
private Project GetWebApiProject()
{
var work = MSBuildWorkspace.Create();
var solution = work.OpenSolutionAsync(PathToSolution).Result;
var project = solution.Projects.FirstOrDefault(p =>
p.Name.ToUpper().EndsWith("WEBAPI"));
if (project == null)
throw new ApplicationException(
"WebApi project not found in solution " + PathToSolution);
return project;
}
si sigue una convención de nomenclatura diferente, puede incorporarla fácilmente al proyecto GetWebApiProject para buscar el proyecto que le interesa.
Ahora que sé con qué proyecto quiero trabajar, necesito obtener la compilación para ese proyecto, así como una referencia al tipo que usaré para identificar los controladores de interés. Necesito la compilación porque usaré SemanticModel para determinar si una clase deriva de System.Web.Http.ApiController. Del proyecto, puedo obtener los documentos incluidos en él. Cada documento es un archivo independiente que puede incluir más de una declaración de clase, aunque el procedimiento recomendado es incluir una única clase en un archivo y que el nombre del archivo coincida con el de la clase, pero no siempre se sigue este estándar.
Buscar los controladores
La Figura 4 muestra cómo buscar todas las declaraciones de clase en cada documento y determinar si la clase se deriva de ApiController.
Figura 4 Buscar los controladores en un proyecto
public IEnumerable<ClassDeclarationSyntax> FindControllers(Project project)
{
compilation = project.GetCompilationAsync().Result;
var targetType = compilation.GetTypeByMetadataName(
"System.Web.Http.ApiController");
foreach (var document in project.Documents)
{
var tree = document.GetSyntaxTreeAsync().Result;
var semanticModel = compilation.GetSemanticModel(tree);
foreach (var type in tree.GetRoot().DescendantNodes().
OfType<ClassDeclarationSyntax>()
.Where(type => GetBaseClasses(semanticModel, type).Contains(targetType)))
{
yield return type;
}
}
}
Dado que la compilación tiene acceso a todas las referencias necesarias para compilar el proyecto, no tendrá problemas para resolver el tipo de destino. Cuando consigo el objeto de compilación, ya he empezado a compilar el proyecto, pero, cuando tengo los detalles para obtener los metadatos necesarios, el proceso se interrumpe antes de finalizar.
La Figura 5 muestra el método GetBaseClasses que realiza el trabajo pesado para determinar si la clase actual deriva de la clase de destino. Esto realiza un poco más de procesamiento del estrictamente necesario. Para determinar si una clase deriva de ApiController, lo importante no son las interfaces implementadas en el proceso, pero, al incluir estos detalles, se convierte en un método útil que se puede usar en una amplia variedad de lugares.
Figura 5 Buscar interfaces y clases base
public static IEnumerable<INamedTypeSymbol> GetBaseClasses
(SemanticModel model, BaseTypeDeclarationSyntax type)
{
var classSymbol = model.GetDeclaredSymbol(type);
var returnValue = new List<INamedTypeSymbol>();
while (classSymbol.BaseType != null)
{
returnValue.Add(classSymbol.BaseType);
if (classSymbol.Interfaces != null)
returnValue.AddRange(classSymbol.Interfaces);
classSymbol = classSymbol.BaseType;
}
return returnValue;
}
Este tipo de análisis se complica con la reflexión porque un enfoque reflexivo depende de la recurrencia y puede necesitar cargar cierta cantidad de ensamblados a lo largo del proceso para tener acceso a todos los tipos implicados. Este tipo de análisis ni siquiera es posible con el modelo de código, pero es relativamente directo con Roslyn si se usa SemanticModel. SemanticModel es un tesoro escondido de metadatos; representa todo lo que sabe el compilador acerca del código después de hacer el trabajo de enlazar árboles de sintaxis a símbolos. Además de realizar el seguimiento de los tipos base, se puede usar para responder a preguntas difíciles como la resolución de sobrecarga o reemplazo, o para buscar todas las referencias a un método, propiedad o cualquier símbolo.
Buscar el modelo asociado
En este punto, tengo acceso a todos los controladores del proyecto. En la clase JavaScript, también es recomendable exponer las propiedades que se encuentran en los modelos que devuelven las acciones del controlador. Para comprender cómo funciona, eche un vistazo al código siguiente, que muestra el resultado de ejecutar scaffolding para WebApi:
public class Activity
{
public int Id { get; set; }
public int ActivityCode { get; set; }
public int ProjectId { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
En este caso, se ejecutó scaffolding en los modelos, como se muestra en la Figura 6.
Figura 6 Controlador de API generado
public class ActivitiesController : ApiController
{
private ApplicationDbContext db = new ApplicationDbContext();
// GET: api/Activities
public IQueryable<Activity> GetActivities()
{
return db.Activities;
}
// GET: api/Activities/5
[ResponseType(typeof(Activity))]
public IHttpActionResult GetActivity(int id)
{
Activity activity = db.Activities.Find(id);
if (activity == null)
{
return NotFound();
}
return Ok(activity);
}
// POST: api/Activities
[ResponseType(typeof(Activity))]
public IHttpActionResult PostActivity(Activity activity)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Activities.Add(activity);
db.SaveChanges();
return CreatedAtRoute("DefaultApi", new { id = activity.Id }, activity);
}
// DELETE: api/Activities/5
[ResponseType(typeof(Activity))]
public IHttpActionResult DeleteActivity(int id)
{
Activity activity = db.Activities.Find(id);
if (activity == null)
{
return NotFound();
}
db.Activities.Remove(activity);
db.SaveChanges();
return Ok(activity);
}
El atributo ResponseType agregado a las acciones vinculará ViewModel al controlador. Al usar este atributo, puede asociar el nombre del modelo con la acción. Si el controlador se creó con scaffolding, todas las acciones se asociarán con el mismo modelo, pero puede que los controladores creados a mano o editados después de su generación no sean tan coherentes. La Figura 7 muestra cómo realizar una comparación con todas las acciones para obtener una lista completa de los modelos asociados con un controlador, en caso de que haya más de uno.
Figura 7 Buscar modelos asociados con un controlador
public IEnumerable<TypeInfo> FindAssociatedModel
(SemanticModel semanticModel, TypeDeclarationSyntax controller)
{
var returnValue = new List<TypeInfo>();
var attributes = controller.DescendantNodes().OfType<AttributeSyntax>()
.Where(a => a.Name.ToString() == "ResponseType");
var parameters = attributes.Select(a =>
a.ArgumentList.Arguments.FirstOrDefault());
var types = parameters.Select(p=>p.Expression).OfType<TypeOfExpressionSyntax>();
foreach (var t in types)
{
var symbol = semanticModel.GetTypeInfo(t.Type);
if (symbol.Type.SpecialType == SpecialType.System_Void) continue;
returnValue.Add( symbol);
}
return returnValue.Distinct();
}
Este método contiene una lógica interesante y parte de ella es bastante sutil. Recuerde el aspecto del atributo ResponseType:
[ResponseType(typeof(Activity))]
Quiero obtener acceso a las propiedades del tipo al que se hace referencia en el tipo de expresión, que es el primer parámetro para el atributo; en este caso, Activity. La variable attributes es una lista de los atributos ResponseType incluidos en el controlador. La variable parameters es una lista de los parámetros para estos atributos. Cada uno de estos parámetros será un TypeOfExpressionSyntax y puedo obtener el tipo asociado mediante la propiedad type de los objetos TypeOfExpressionSyntax. De nuevo, se usa SemanticModel para extraer el valor de Symbol para dicho tipo, lo que le proporciona todos los detalles que pueda necesitar.
La cláusula Distinct al final del método garantiza que todos los modelos que se devuelvan son únicos. En la mayoría de circunstancias, cabe esperar que haya duplicados porque varias acciones del controlador se asociarán con el mismo modelo. También es buena idea comprobar el elemento ResponseType que se anula. Allí no encontrará propiedades interesantes.
Examinar el modelo asociado
El código siguiente muestra cómo buscar las propiedades de todos los modelos que contiene el controlador:
public IEnumerable<ISymbol> GetProperties(IEnumerable<TypeInfo> models)
{
return models.Select(typeInfo => typeInfo.Type.GetMembers()
.Where(m => m.Kind == SymbolKind.Property))
.SelectMany(properties => properties).Distinct();
}
Buscar las acciones
Además de mostrar las propiedades de los modelos asociados, quiero incluir referencias a los métodos que contiene el controlador. Los métodos de un controlador son acciones. Solo me interesan los métodos públicos y, como son acciones de WebApi, todos se deben traducir al verbo HTTP correspondiente.
Para controlar esta asignación, se pueden seguir un par de convenciones distintas. La que sigue scaffolding consiste en que el nombre del método empiece por el nombre del verbo. Así pues, el método put sería PutActivity, el método post sería PostActivity, el método delete sería DeleteActivity, etc. En general, habrá dos métodos get: GetActivity y GetActivities. Para ver la diferencia entre los métodos get, examine sus respectivos tipos de valores devueltos. Si el tipo de valor devuelto implementa directa o indirectamente la interfaz IEnumerable, el método get es get all; de lo contrario, se trata del método get single.
El otro enfoque consiste en agregar atributos explícitamente para especificar el verbo. En ese caso, el método podría tener cualquier nombre. La Figura 8 muestra el código para GetActions que identifica los métodos públicos y, a continuación, los asigna a verbos que usan ambos métodos.
Figura 8 Buscar acciones en un controlador
public IEnumerable<string> GetActions(ClassDeclarationSyntax controller)
{
var semanticModel = compilation.GetSemanticModel(controller.SyntaxTree);
var actions = controller.Members.OfType<MethodDeclarationSyntax>();
var returnValue = new List<string>();
foreach (var action in actions.Where
(a => a.Modifiers.Any(m => m.Kind() == SyntaxKind.PublicKeyword)))
{
var mapName = MapByMethodName(semanticModel, action);
if (mapName != null)
returnValue.Add(mapName);
else
{
mapName = MapByAttribute(semanticModel, action);
if (mapName != null)
returnValue.Add(mapName);
}
}
return returnValue.Distinct();
}
En primer lugar, el método GetActions intenta realizar la asignación en función del nombre del método. Si eso no funciona, intenta asignarlo en función de los atributos. Si el método no se puede asignar, no se incluye en la lista de acciones. Si tiene una convención distinta que quiere comprobar, puede incorporarla fácilmente en el método GetActions. La Figura 9 muestra las implementaciones para los métodos MapByMethodName y MapByAttribute.
Figura 9 MapByName y MapByAttribute
private static string MapByAttribute(SemanticModel semanticModel,
MethodDeclarationSyntax action)
{
var attributes = action.DescendantNodes().OfType<AttributeSyntax>().ToList();
if ( attributes.Any(a=>a.Name.ToString() == "HttpGet"))
return IdentifyIEnumerable(semanticModel, action) ? "query" : "get";
var targetAttribute = attributes.FirstOrDefault(a =>
a.Name.ToString().StartsWith("Http"));
return targetAttribute?.Name.ToString().Replace("Http", "").ToLower();
}
private static string MapByMethodName(SemanticModel semanticModel,
MethodDeclarationSyntax action)
{
if (action.Identifier.Text.Contains("Get"))
return IdentifyIEnumerable(semanticModel, action) ? "query" : "get";
var regex = new Regex("\b(?'verb'post|put|delete)", RegexOptions.IgnoreCase);
if (regex.IsMatch(action.Identifier.Text))
return regex.Matches(action.Identifier.Text)[0]
.Groups["verb"].Value.ToLower();
return null;
}
Para empezar, ambos métodos buscan explícitamente la acción get y determinan el tipo de "get" al que se refiere el método.
Si la acción no es una opción de "get", MapByAttribute comprueba si tiene un atributo que empieza por Http. Si lo tiene, para determinar el verbo, solo hay que tomar el nombre del atributo y quitarle Http. No es necesario comprobar explícitamente todos los atributos para determinar qué verbo se debe usar.
La estructura de MapByMethodName es parecida. Después de buscar una acción get en primer lugar, este método usa una expresión regular para ver si algún otro verbo coincide. Si se encuentra una correspondencia, puede obtener el nombre del verbo del grupo de capturas con nombres.
Ambos métodos de asignación deben distinguir entre las acciones get single y get all. Además, ambos usan el método IdentifyEnumerable que se muestra en el código siguiente:
private static bool IdentifyIEnumerable(SemanticModel semanticModel,
MethodDeclarationSyntax actiol2n)
{
var symbol = semanticModel.GetSymbolInfo(action.ReturnType);
var typeSymbol = symbol.Symbol as ITypeSymbol;
if (typeSymbol == null) return false;
return typeSymbol.AllInterfaces.Any(i => i.Name == "IEnumerable");
}
De nuevo, SemanticModel tiene un papel fundamental. Para diferenciar los métodos get, examine el tipo de valor devuelto del método. SemanticModel devolverá el símbolo vinculado con el tipo de valor devuelto. Con este símbolo, puedo saber si el tipo de valor devuelto implementa la interfaz IEnumerable. Si el método devuelve List<T>, Enumerable<T> o cualquier tipo de colección, implementará la interfaz IEnumerable.
La plantilla T4
Ahora que ya se han recopilado todos los metadatos, ha llegado el momento de visitar la plantilla T4 que juntará todas las piezas. Para empezar, agrego una plantilla de texto de entorno en tiempo de ejecución al proyecto.
Para una plantilla de texto de entorno en tiempo de ejecución, la salida resultante de ejecutar la plantilla será una clase que implementará la clase que se define y no el código que quiero producir. En la mayoría de casos, lo que se puede hacer en una plantilla de texto, también se puede hacer en una plantilla de texto de entorno en tiempo de ejecución. La diferencia es la forma de ejecutar la plantilla para generar código. Con una plantilla de texto, Visual Studio controlará la ejecución de la plantilla y la creación del entorno de hospedaje en el que se ejecutará la plantilla. Con una plantilla de texto de entorno en tiempo de ejecución, tendrá la responsabilidad de configurar el entorno de hospedaje y ejecutar la plantilla. Esto supone más trabajo, pero también le ofrece mucho más control sobre la forma de ejecutar la plantilla y lo que hace con la salida. También quita cualquier dependencia de Visual Studio.
Para empezar, edito AngularResource.tt y agrego el código de la Figura 10 a la plantilla.
Figura 10 Plantilla inicial
<#@ template debug="false" hostspecific="false" language="C#" | #>
var app = angular.module("challenge", [ "ngResource"]);
app.factory(<#=className #>Resource . function ($resource) {
return $resource('<#=Url#>/<#=className#>',{<=className#> : '@<#=className#>'},{
<#=property.Name#> : "",
query : {
method: "GET"
, isArray : true
}
' <#=action#>: {
method: "<#= action.ToUpper()#>
}
});
});
Según lo familiarizado que esté con JavaScript, esto podría ser nuevo para usted. Si lo es, no se preocupe.
La primera línea es la directiva de plantilla e indica a T4 que voy a escribir código de plantilla en C#; los otros dos atributos se ignoran para las plantillas de entorno en tiempo de ejecución, pero, para que quede más claro, indico explícitamente que no tengo ninguna expectativa del entorno de hospedaje y que no espero que se conserven los archivos intermedios para la depuración.
La plantilla T4 se parece un poco a una página de ASP. Las etiquetas <# y #> indican límites en el código para que la plantilla pueda transformar la plantilla y el texto. Las etiquetas <#= #> delimitan una sustitución de variable que se debe evaluar e insertar en el código generado.
Al observar esta plantilla, puede ver que se espera que los metadatos proporcionen un valor de className, una dirección URL, una lista de propiedades y una lista de acciones. Dado que se trata de una plantilla de entorno en tiempo de ejecución, puedo hacer un par de cosas para simplificar, pero eche un vistazo antes al código que se crea al ejecutar esta plantilla, lo que se realiza al guardar el archivo .TT o al hacer clic con el botón derecho en el archivo en el Explorador de soluciones y seleccionar Ejecutar herramienta personalizada.
La salida resultante de ejecutar la plantilla es una clase nueva correspondiente a la plantilla. Más importante, si desplazo hacia abajo, descubriré que la plantilla también generó la clase base. Esto es importante porque, si muevo la clase base a un archivo nuevo e indico explícitamente la clase base en la directiva de plantilla, ya no se generará y podré cambiar esta clase base como sea necesario.
A continuación, cambiaré la directiva de plantilla así:
<#@ template debug="false" hostspecific="false" language="C#"
inherits="AngularResourceServiceBase" #>
Después, moveré AngularResourceServiveBase a su propio archivo. Cuando vuelva a ejecutar la plantilla, veré que la clase generada sigue derivando de la misma clase base, pero que ya no se generó. Ahora ya puedo aplicar los cambios necesarios a la clase base.
A continuación, agregaré algunos métodos nuevos y un par de propiedades a la clase base para que resulte más fácil proporcionar los metadatos a la plantilla.
Para dar cabida a los métodos y propiedades nuevos, también necesitaré algunos nuevos con instrucciones:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Agregaré propiedades para la dirección URL y para el elemento RoslynDataProvider que creé al principio del artículo:
public string Url { get; set; }
public RoslynDataProvider MetadataProvider { get; set; }
Con estas piezas en su lugar, también necesitaré un par de métodos que interactuarán con MetadataProvider, como se muestra en la Figura 11.
Figura 11 Métodos auxiliares agregados a AngularResourceServiceBase
public IList<ClassDeclarationSyntax> GetControllers()
{
var project = MetadataProvider.GetWebApiProject();
return MetadataProvider.FindControllers(project).ToList();
}
protected IEnumerable<string> GetActions(ClassDeclarationSyntax controller)
{
return MetadataProvider.GetActions(controller);
}
protected IEnumerable<TypeInfo> GetModels(ClassDeclarationSyntax controller)
{
return MetadataProvider.GetModels(controller);
}
protected IEnumerable<ISymbol> GetProperties(IEnumerable<TypeInfo> models)
{
return MetadataProvider.GetProperties(models);
}
Ahora que he agregado estos métodos a la clase base, estoy listo para ampliar la plantilla para usarlos. Observe cómo cambia la plantilla en la Figura 12.
Figura 12 Versión final de la plantilla
<#@ template debug="false" hostspecific="false" language="C#" inherits="AngularResourceServiceBase" #>
var app = angular.module("challenge", [ "ngResource"]);
<#
var controllers = GetControllers();
foreach(var controller in controllers)
{
var className = controller.Identifier.Text.Replace("Controller", "");
#> app.facctory(<#=className #>Resource , function ($resource) {
return $resource('<#=Url#>/<#=className#>',{<#=className#> : '@<#=className#>'},{
<#
var models= GetModels(controller);
var properties = GetProperties(models);
foreach (var property in properties)
{
#>
<#=property.Name#> : "",
<#
}
var actions = GetActions(controller);
foreach (var action in actions)
{
#>
<#
if (action == "query")
{
#> query : {
method: "GET"
Ejecutar la plantilla
Dado que se trata de una plantilla de entorno en tiempo de ejecución, tengo la responsabilidad de configurar el entorno para ejecutarla. La Figura 13 muestra el código necesario para ejecutarla.
Figura 13 Ejecutar una plantilla de texto de entorno en tiempo de ejecución
private static void Main()
{
var work = MSBuildWorkspace.Create();
var solution = work.OpenSolutionAsync(Path to the Solution File).Result;
var metadata = new RoslynDataProvider() {Workspace = work};
var template = new AngularResourceService
{
MetadataProvider = metadata,
Url = @"http://localhost:53595/"
};
var results = template.TransformText();
var project = metadata.GetWebApiProject();
var folders = new List<string>() { "Scripts" };
var document = project.AddDocument("factories.js", results, folders)
.WithSourceCodeKind(SourceCodeKind.Script)
;
if (!work.TryApplyChanges(document.Project.Solution))
Console.WriteLine("Failed to add the generated code to the project");
Console.WriteLine(results);
Console.ReadLine();
}
Se pueden crear instantáneas directamente de la clase que se crea al guardar la plantilla o ejecutar la herramienta personalizada y puedo definir cualquier propiedad pública u obtener acceso a ella, así como llamar a cualquier método público de la clase base. Así es como se definen los valores de las propiedades. Al llamar al método TransformText, se ejecuta la plantilla y se devuelve el código generado como cadena. La variable results contendrá el código generado. El resto del código se ocupa de agregar un documento nuevo al proyecto con el código que se generó.
Sin embargo, este código tiene un problema. La llamada a AddDocuments crea correctamente un documento y lo coloca en la carpeta de scripts. Cuando llamo a TryApplyChanges, el resultado es correcto. El problema viene cuando miro la solución: Hay un archivo factories en la carpeta de scripts. El problema es que, en lugar de factories.js, es factories.cs. El método AddDocument no está configurado para aceptar una extensión. Independientemente de la extensión, el documento se agregará en función del tipo de proyecto al que se agregue. Esto es estructural.
Por lo tanto, después de que el programa se ejecute y genere las clases JavaScript, el archivo estará en la carpeta de scripts. Solo tengo que cambiar la extensión de .cs a .js.
Resumen
La mayor parte del trabajo que hemos hecho aquí se centra en la obtención de metadatos con Roslyn. Independientemente de cómo tenga previsto usar los metadatos, estas mismas prácticas resultarán útiles. Respecto al código de T4, seguirá siendo relevante en varios lugares. Si quiere generar código para cualquier lenguaje que Roslyn no admita, T4 es una forma fantástica y fácil de incorporarlo a los procesos. Esto es positivo porque Roslyn solo se puede usar para generar código para C# y Visual Basic .NET, mientras que T4 le permite generar cualquier tipo de texto, que puede ser SQL, JavaScript, HTML, CSS o incluso el antiguo texto sin formato.
Es útil para generar código como estas clases JavaScript porque son tediosas y propensas a errores. También se ajustan a un patrón fácilmente. En la medida de lo posible, es recomendable seguir ese patrón de un modo tan coherente como sea posible. Además, lo más importante, el aspecto que quiere que tenga el código generado puede cambiar con el tiempo, especialmente para algo nuevo, a medida que se definan procedimientos recomendados. Si solo tiene que actualizar una plantilla T4 para cambiar a la nueva "mejor forma de hacerlo", es más probable que se adhiera a los procedimientos recomendados emergentes. En cambio, si tiene que modificar grandes cantidades de código tedioso y monótono generado a mano, probablemente tendrá muchas implementaciones, según el procedimiento recomendado de moda.
Nick Harrison es consultor de software y reside en Columbia, S.C., con su mujer y su hija. Ha estado desarrollando una pila completa con .NET para crear una soluciones empresariales desde 2002. Puede ponerse en contacto con él a través de Twitter (@Neh123us), donde también anuncia sus publicaciones de blog, artículos publicados y charlas.
Gracias al siguiente experto técnico de Microsoft por revisar este artículo: James McCaffrey
El Dr. James McCaffrey trabaja para Microsoft Research en Redmond, Washington. Ha colaborado en el desarrollo de varios productos de Microsoft como, por ejemplo, Internet Explorer y Bing. Puede ponerse en contacto con el Dr. McCaffrey en jammc@microsoft.com.