Dezembro de 2016

Volume 31 - Número 13

Roslyn - Gerar JavaScript com Roslyn e Modelos T4

Por Nick Harrison | Dezembro de 2016

Outro dia, minha filha me contou uma piada sobre uma conversa entre um smartphone e um feature phone. Era algo mais ou menos assim: O que um smartphone disse para um feature phone? “Eu vim do futuro, você consegue me entender?” Algumas vezes temos essa sensação ao aprender algo novo e inovador. O Roslyn vem do futuro e pode ser difícil entendê-lo no começo.

Neste artigo, discutirei o Roslyn de um modo que pode não ter tanto foco como necessário. Manterei o foco no uso do Roslyn como uma fonte de metadados para gerar JavaScript com T4. Isso usará a Workspace API, um pouco da Syntax API, a Symbol API e um modelo de tempo de execução para o mecanismo T4. O JavaScript gerado é secundário para entender os processos usados para reunir os metadados.

Como o Roslyn também fornece algumas boas opções para a geração de código, você poderia pensar que as duas tecnologias não seriam compatíveis e não funcionariam em conjunto. Tecnologias frequentemente entram em conflito quando áreas restritas se sobrepõem, mas essas duas tecnologias funcionam em conjunto bastante bem.

Espere... o que é T4?

Se o T4 é uma novidade para você, um e-book de 2015 da série Syncfusion Succinctly, “T4 Succinctly,” fornece todas as informações de que você precisa (bit.ly/2cOtWuN).

No momento, o principal ponto a saber é que o T4 é o toolkit de transformação de texto com base em modelos da Microsoft. Você alimenta metadados para o modelo e o texto se torna o código desejado. Na realidade, você não está limitado a código. Você pode gerar qualquer tipo de texto, mas código-fonte é o tipo mais comum de saída. Você pode gerar HTML, SQL, documentação em texto, Visual Basic .NET, C# ou qualquer saída com base em texto.

Observe a Figura 1. Ela mostra um programa simples de Aplicativo Console. No Visual Studio, adicionei um novo modelo de texto de tempo de execução chamado AngularResourceService.tt. O código do modelo gera automaticamente código C# que implementará o modelo no momento da execução, o que pode ser visto na janela do console.

Usando o T4 para Geração de Código no Momento do Design
Figura 1 Usando o T4 para Geração de Código no Momento do Design

Neste artigo, mostrarei como usar o Roslyn para reunir metadados de um projeto de Web API para alimentação ao T4 para gerar uma classe JavaScript e, em seguida, usar o Roslyn para adicionar esse JavaScript de volta à solução.

Conceitualmente, o fluxo do processo se parecerá com a Figura 2.

Fluxo de Processo T4
Figura 2 Fluxo de Processo T4

Roslyn Alimenta T4

A geração de código é um processo que exige metadados. Você precisa de metadados para descrever o código que deseja gerar. Reflexão, Modelo de Código e o Dicionário de Dados são fontes comuns de metadados prontamente disponíveis. O Roslyn pode fornecer todos os metadados que você receberia de Reflexão ou do Modelo de Código, mas sem alguns dos problemas que essas outras abordagens causam.

Neste artigo, usarei o Roslyn para encontrar classes derivadas do ApiController. Usarei o modelo T4 para criar uma classe JavaScript para cada Controlador e expôr um método para cada Ação e uma propriedade para cada propriedade no ViewModel associado com o Controlador. O resultado se parecerá com o código na Figura 3.

Figura 3 Resultado da Execução do 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"
    }
  });
});

Reunindo os Metadados

Começarei reunindo os metadados com a criação de um novo projeto de aplicativo console no Visual Studio 2015. Neste projeto, terei uma classe dedicada a reunir metadados com o Roslyn, assim como um modelo T4. Isso será um modelo de tempo de execução que gerará código JavaScript com base nos metadados reunidos.

Uma vez que o projeto tenha sido criado, os seguintes comandos do Console de Gerenciador de Pacote são emitidos:

Install-Package Microsoft.CodeAnalysis.CSharp.Workspaces

Isso assegura que o código mais recente do Roslyn para o compilador CSharp e serviços relacionados estão sendo usados.

Colocarei o código para os diversos métodos em uma nova classe chamada RoslynDataProvider. Ao longo deste artigo, farei referência a essa classe e será uma referência útil sempre que eu desejar reunir metadados com o Roslyn.

Usarei o MSBuildWorksspace para obter um espaço de trabalho que fornecerá todo o contexto necessário para a compilação. Uma vez que eu tenha a solução, posso navegar facilmente pelos projetos, buscando o projeto 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;
}

Se você utilizar uma convenção de nomenclatura diferente, você pode incorporá-la facilmente em GetWebApiProject para encontrar o projeto no qual está interessado.

Agora que já sei o projeto com o qual desejo trabalhar, preciso obter a compilação para esse projeto, bem como a referência para o tipo que usarei para identificar os controles de interesse. Preciso da compilação porque usarei o SemanticModel para determinar se uma classe deriva de System.Web.Http.ApiController. A partir do projeto, posso obter os documentos incluídos no projeto. Cada documento é um arquivo separado, que poderia incluir mais do que uma declaração de classe, embora incluir apenas uma única classe em qualquer arquivo e fazer com que o nome do arquivo corresponda ao nome da classe seja uma prática recomendada. Porém, nem todos seguem esse padrão o tempo todo.

Encontrando os Controladores

A Figura 4 mostra como encontrar todas as declarações de classe em todos os documentos e determinar se a classe é derivada de ApiController.

Figura 4 Encontrando os Controladores em um Projeto

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;
    }
  }
}

Como a compilação tem acesso a todas as referências necessárias para compilar o projeto, não haverá problemas para resolver o tipo de destino. Quando obtenho o objeto de compilação, comecei por compilar o projeto, mas fui interrompido na metade do caminho uma vez que tive os detalhes para obter os metadados necessários.

A Figura 5 mostra o método GetBaseClasses que realiza o trabalho pesado para determinar se a classe atual deriva da classe de destino. Isso executa um pouco mais de processamento do que o estritamente necessário. Para determinar se uma classe é derivada de ApiController, não preciso me ater às interfaces implementadas pelo caminho, mas, incluindo esses detalhes, isso se torna um método de utilitário prático que pode ser usado em uma ampla variedade de lugares.

Figura 5 Encontrando Classes e Interfaces de 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;
}

Esse tipo de análise se torna complicado com a Reflexão, já que uma abordagem reflexiva se baseia em recursão e precisa potencialmente carregar um número de assemblies ao longo do percurso para obter acesso a todos os tipos interventores. Esse tipo de análise não é possível com o Modelo de Código, mas é relativamente simples com o Roslyn usando o SemanticModel. O SemanticModel é um baú de tesouro de metadados. Representa tudo o que o compilador saber sobre o código após realizar o trabalho de associar as árvores sintáticas aos símbolos. Além de rastrear os tipos de base, pode ser usado para responder a perguntas difíceis como a resolução de sobrecarga/substituição ou para encontrar todas as referências a um método ou propriedade ou qualquer Símbolo.

Encontrando o Modelo Associado

Nessa altura, tenho acesso a todos os Controladores no projeto. Na classe JavaScript, também é bom expor as propriedades encontradas nos Modelos retornados pelas Ações no Controlador. Para entender como isso funciona, observe o código a seguir, que mostra a saída da execução de scaffolding para uma 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; }
  }

Neste caso, o scaffolding foi executado contra os Modelos, como mostrado na Figura 6.

Figura 6 Controlador de API Gerado

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);
    }

O atributo ResponseType adicionado às ações conectará o ViewModel ao Controlador. Usando esse atributo, você pode obter o nome do modelo associado à ação. Desde que o Controlador tenha sido criado usando scaffolding, toda ação será associada com o mesmo modelo, mas os Controladores criados à mão ou editados após serem gerados podem não ser tão consistentes. A Figura 7 mostra como comparar contra todas as ações para obter uma lista completa dos modelos associados com um Controlador, caso haja mais do que um.

Figura 7 Encontrando Modelos Associados com um 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();
}

Há uma lógica interessante acontecendo neste método, parte da qual é bastante sutil. Lembre-se de que como atributo ResponseType se parece:

[ResponseType(typeof(Activity))]

Quero acessar as propriedade no tipo referenciado no tipo de expressão, que é o primeiro parâmetro ao atributo, neste caso, Atividade. A variável dos atributos é uma lista dos atributos ResponseType encontrados no controlador. A variável dos parâmetros é uma lista dos parâmetros para esses atributos. Cada um desses parâmetros será um TypeOfExpressionSyntax e posso obter o tipo associado por meio da propriedade de tipo dos objetos TypeOfExpressionSyntax. Novamente, o SemanticModel é usado para efetuar pull do Símbolo para esse tipo, o que fornecerá todos os detalhes que você deseja.

Distinguir ao final do método assegurará que cada modelo retornado é único. Na maioria das circunstâncias, você esperaria ter duplicações porque várias ações no Controlador serão associadas com o mesmo modelo. É uma boa ideia verificar se o ResponseType está vazio. Você não encontrará nenhuma propriedade interessante aqui.

Examinando o Modelo Associado

O código a seguir mostra como encontrar as propriedade de todos os modelos encontrados no 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();
}

Encontrando as Ações

Além de mostrar as propriedades dos Modelos Associados, quero incluir referências aos métodos que estão no Controlador. Os métodos em um Controlador são Ações. Estou interessado apenas nos métodos Públicos e, como estes são Ações WebApi, devem ser traduzidos para o Verbo HTTP adequado.

Há duas convenções diferentes para lidar com este mapeamento. A convenção seguida pelo scaffolding é para que o nome de método se inicie com o nome do verbo. Dessa forma, o método put seria PutActivity, o método post seria PostActivity, o método delete seria DeleteActivity e, geralmente, haverá dois métodos get: GetActivity e GetActivities. Você pode perceber a diferença entre os métodos get examinando os tipos de retorno para esses métodos. Se o tipo de retorno implementa direta ou indiretamente a interface IEnumerable, o método get é get all, caso contrário, é o método de item get single.

A outra abordagem é adicionar explicitamente atributos para especificar o verbo, assim o método poderá ter qualquer nome. A Figura 8 mostra o código para GetActions que identifica os métodos públicos e, em seguida, mapeia-os para verbos usando os dois métodos.

Figura 8 Encontrando Ações em um 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();
}

O método GetActions primeiro tenta mapear com base no nome do método. Se isso não funciona, tentará mapear pelos atributos. Se o método não puder ser mapeado, ele não será incluído na lista de ações. Se você tem uma convenção diferente contra a qual deseja verificar, você pode incorporá-la facilmente no método GetActions. A Figura 9 mostra as implementações para os métodos MapByMethodName e MapByAttribute.

Figura 9 MapByName e 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;
}

Ambos os métodos começam buscando explicitamente por Get Action e determinando qual tipo de “get” ao qual o método se refere.

Se a ação não é um dos “gets”, MapByAttribute verificará para ver se a ação tem um atributo que começa com Http. Se um for encontrado, o verbo pode ser determinado de maneira simples, removendo Http do nome do atributo. Não há necessidade de se verificar explicitamente contra cada atributo para determinar qual verbo deverá ser usado.

MapByMethodName é estruturado de maneira similar. Após buscar por uma ação Get, esse método usa uma expressão regular para verificar se algum dos outros verbos é uma correspondência. Se uma correspondência for encontrada, você pode obter o nome do verbo do grupo de captura nomeado.

Ambos os métodos de mapeamento precisam diferenciar entre Ações Get Single e Get All e ambos usam o método Identify­Enumerable mostrado no código a seguir:

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");
}

Novamente, o SemanticModel tem um papel central. Posso diferenciar entre os métodos get examinando o tipo de retorno do método. O SemanticModel retornará o símbolo associado ao tipo de retorno. Com esse símbolo, posso dizer se o tipo de retorno implementa a interface IEnumerable. Se o método retorna uma List<T> ou um Enumerable<T> ou qualquer tipo de coleção, ele implementará a interface IEnumerable.

O Modelo T4

Agora que todos os metadados estão reunidos, é o momento de visitar o modelo T4 que irá juntar esses pedaços. Começo adicionando um Modelo de Texto de Tempo de Execução ao projeto.

Para um Modelo de Texto de Tempo de Execução, a saída da execução do modelo será uma classe que implementará o modelo que é definido, e não o código que desejo produzir. Na maioria dos casos, tudo o que você pode fazer em um Modelo de Texto pode ser feito com um Modelo de Texto de Tempo de Execução. A diferença é o modo como você executa o modelo para gerar o código. Com um Modelo de Texto, o Visual Studio se encarregará da execução do modelo e criará o ambiente de hospedagem no qual o modelo será executado. Com um Modelo de Texto de Tempo de Execução, você é responsável pela configuração do ambiente de hospedagem e pela execução do modelo. Isso gera mais trabalho para você, mas também oferece muito mais controle sobre o modo como você executa o modelo e o que você pode fazer com a saída. Também remove qualquer dependência quando ao Visual Studio.

Comecei editando o AngularResource.tt adicionando o código na Figura 10 ao modelo.

Figura 10 Modelo 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()#>
    }
  });
});

Dependendo de o quanto você está familiarizado com o JavaScript, o que pode ser novo para você — neste caso, não se preocupe.

A primeira linha é a diretiva do modelo e informa ao T4 que eu escreverei o código modelo em C#. Os outros dois atributos são ignorados para modelos de Tempo de Execução, mas fins de clareza, estou declarando explicitamente que não tenho nenhuma expectativa quanto ao ambiente de hospedagem e não espero que arquivos intermediários sejam conservados para depuração.

O modelo T4 é parecido com uma página ASP. As marcações <# e #> delimitam entre o código para conduzir o modelo e o texto a serem transformados pelo modelo. As marcações <#=#> delimitam uma substituição de variável que será avaliada e inserida no código gerado.

Observando o modelo, você pode ver que se espera que os metadados forneçam uma className, URL, uma lista de propriedades e uma lista de ações. Como se trata de um modelo de Tempo de Execução, há duas coisas que posso fazer para simplificar o processo. Mas, primeiramente, observe o código que é criado quando o modelo é executado, o que é feito salvando o arquivo .TT ou clicando com o botão direito do mouse no arquivo no Gerenciador de Soluções e selecionando Executar Ferramenta Personalizada.

O resultado da execução do modelo é uma nova classe, que corresponde ao modelo. Mais importante, se eu rolar para baixo, descobrirei que o modelo também gerou a classe base. Isso é importante porque se eu mover a classe base para um novo arquivo e declarar explicitamente a classe base na diretiva do modelo, ela não será mais gerada e estarei livre para alterar essa classe base conforme necessário.

Em seguida, modificarei a diretiva do modelo da seguinte forma:

<#@ template debug="false" hostspecific="false" language="C#"
  inherits="AngularResourceServiceBase" #>

A seguir, moverei AngularResourceServiveBase para seu próprio arquivo. Quando eu executar o modelo novamente, verei que a classe gerada ainda deriva da mesma classe base, mas ela não foi mais gerada. Agora, estou livre para realizar quaisquer alterações necessárias na classe base.

A seguir, adicionarei alguns novos métodos e duas propriedades à classe base para fazer com que seja mais fácil fornecer os metadados para o modelo.

Para acomodar os novos métodos e propriedades, também preciso usar algumas novas declarações:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Adicionarei propriedades para o URL e para o RoslynDataProvider que criei no início do artigo:

public string Url { get; set; }
public RoslynDataProvider MetadataProvider { get; set; }

Com essas partes concluídas, também precisarei de dois métodos que interagirão com o MetadataProvider, como mostrado na Figura 11.

Figura 11 Métodos Auxiliares Adicionados 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);
}

Agora que adicionei esses métodos à classe base, estou pronto para estender o modelo para usá-los. Observe como o modelo muda na Figura 12.

Figura 12 Versão Final do Modelo

<#@ 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"

Executando o Modelo

Como se trata de um modelo de Tempo de Execução, sou responsável pela configuração do ambiente para a execução do modelo. A Figura 13 mostra o código necessário para executar o modelo.

Figura 13 Executando um Modelo de Texto de Tempo de Execução

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();
}

A classe criada quando salvo o modelo ou executo a ferramenta personalizada pode ser instanciada diretamente e eu posso definir ou acessar quaisquer propriedades públicas ou chamar quaisquer métodos públicos da classe base. Essa é a forma com a qual os valores para as propriedades são definidos. A chamada para o método TransformText executará o modelo e retornará o código gerado como uma cadeia de caracteres. A variável de resultados conterá o código gerado. O resto do código lida com a adição de um novo documento ao projeto com o código que foi gerado.

Porém, há um problema com esse código. A chamada para AddDocuments cria com êxito um documento e o coloca na pasta de scripts. Quando chamo TryApplyChanges, o retorno é exitoso. O problema pode ser visto quando observo a solução: Há um arquivo de fábrica na pasta scripts. O problema é que em vez de factories.js, é factories.cs. O método AddDocument não está configurado para aceitar uma extensão. Independentemente da extensão, o documento será adicionado com base no tipo de projeto ao qual é adicionado. Isso é intrínseco ao projeto.

Portanto, após o programa ter executado e gerado as classes JavaScript, o arquivo estará na pasta de scripts. Tudo o que devo fazer é alterar a extensão de .cs para .js.

Conclusão

A maior parte do trabalho feito aqui é centrado em obter metadados com o Roslyn. Independentemente de como você planeja usar esses metadados, as mesmas práticas serão úteis. Quando ao código T4, ele continuará a ser relevante em vários lugares. Se você deseja gerar código para qualquer linguagem não compatível com o Roslyn, o T4 é uma ótima opção e é fácil de incorporar em seu processo. Isso é bom porque você pode usar o Roslyn para gerar código apenas para C# e Visual Basic .NET, enquanto o T4 permite que você gere qualquer tipo de texto, que pode ser SQL, JavaScript, HTML, CSS ou até mesmo texto sem formatação.

É bom gerar código como essas classes JavaScript, já que é um processo tedioso e passível de gerar erros. Eles também se conformam facilmente a um padrão. Sempre que possível, é bom seguir o padrão o mais consistentemente possível. E o mais importante: o modo como você deseja que o código seja gerado é passível de mudanças ao longo do tempo, especialmente para algo novo, conforme as práticas recomendadas são geradas. Se tudo o que você tiver que fazer é atualizar um modelo T4 para alterar o novo “melhor método para fazer isso”, é mais fácil aderir às práticas recomendadas. Porém, se você tiver que modificar grandes quantidades de código tedioso e gerado à mão, você provavelmente terá várias implementações conforme cada iteração da prática recomendada esteja em voga.


Nick Harrison  é um consultor de software e vive em Columbia, S.C., com sua querida esposa Tracy e sua filha. Tem desenvolvido a todo o vapor usando .NET para criar soluções de negócios desde 2002. Entre em contato com ele pelo Twitter: @Neh123us, onde ele anuncia publicações em seu blog e palestras.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: James McCaffrey
Entre em contato com o Dr. James McCaffrey trabalha para a Microsoft Research em Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo Internet Explorer e Bing. Entre em contato com o Dr. McCaffrey pelo email jammc@microsoft.com.