Templates T4

Introdução

Uma das áreas que me tem interessado e motivado mais nos últimos tempos, é a de geração automática de código, especialmente aplicado a frameworks aplicacionais. Depois de passar horas a escrever código à “moda antiga”, a escrever o mesmo tipo de código constantemente, a ideia de poder gerar automaticamente o código é sem dúvida entusiasmante. E torna-se ainda mais interessante se poder gerar uma estrutura de domínio completa a partir de um pequeno bloco de informação – uma framework que reduz o tempo de desenvolver código tediante. Não me refiro a código que uma melhor estrutura de objectos e hierarquias de herança resolva: refiro-me a objectos que apenas varia nos nomes da classe ou dos campos ou tipo de dados, e que Generics (ou equivalente) não resolvem completamente.

Há várias formas de poder gerar o código de forma automática. É necessário pelo menos um conjunto de metadados (que descreve os objectos do nosso domínio) e um processador para transformar essa informação. O conjunto de metadados pode ser XML, estrutura ou dados de bases de dados, ou um simples ficheiro texto devidamente formatado. É conveniente que os dados sejam estruturados para maximizar as potencialidades. Se o processador se basear em templates, basta aplicar um ou mais templates para transformar os metadados em ficheiros com código funcional. E podemos evoluir as saídas de forma dinâmica. Qualquer alteração a um template altera todos os objectos criados: qualquer bug genérico introduzido num template é corrigido em todos os objectos criados.

E o melhor de tudo, o caro leitor carrega num botão e tem milhares e milhares de linhas de código escrito automaticamente!!! Deve ser claro que não é a “solução maravilha” para todas as situações, nem a “bala prateada”, mas resolve bem muitas situações frequentes, e ficamos com tempo para fazer o que realmente interessa que é atacar os problemas e algoritmos mais complexos.

Text Template Transformation Toolkit (ou T4 para os amigos)

Uma das muitas formas de gerar código é a utilização de Templates T4 (Text Template Transformation Toolkit). O T4 é um motor de processamento de templates integrado directamente no Visual Studio 2008 (para o 2005 é necessário instalar o DSL Toolkit), e permite adicionar e processar automaticamente os templates num projecto de biblioteca de classes do VS. É tão simples como criar um ficheiro com a extensão .tt no projecto que representa um template específico. Em qualquer momento em que o ficheiro de template é armazenado, o template é processado e o ficheiro resultante é criado (ou actualizado). Por defeito, este ficheiro é da extensão do ambiente (.cs se desenvolveres em C#, por exemplo) e incluído imediatamente no projecto em que está inserido, entrando no processo de compilação. No entanto, é possível definir que o ficheiro resultante tenha outro tipo de extensão, como .html, .sql, .aspx, .ascx, .xml, etc.

Sempre que é criado um ficheiro template, é automaticamente criado um ficheiro vazio com o mesmo nome e a extensão de resultado por defeito (normalmente .cs ou .vb, dependendo das definições do ambiente). O template é, essencialmente, um ficheiro com o script de transformação. Cada template resulta num novo ficheiro, e o conteúdo é o resultado do script de transformação. O resultado pode ser qualquer tipo de ficheiro textual – código C# ou VB, ASPX, HTML, Javascript, XML, ou texto. A conversão é precedida de uma compilação do script para prevenir erros.

O processo de transformação dos templates é relativamente complexa, especialmente se construirmos o nosso próprio processador de templates, mas é o que o torna poderoso. De uma forma sucinta, o nosso template (.tt) é transformado numa classe com o nome do ficheiro do template, num espaço de nomes temporário usado pelo Visual Studio. O texto e instruções que temos no template é convertido em instruções (essencialmente de Write() e WriteLine()) de um método da classe criada chamado TransformText(). Se tudo correr bem neste processo, esta classe é compilada e instanciado pelo motor de processamento, e o resultado do TransformText() é escrito para o ficheiro de saída.

Vamos então analisar a sintaxe de um template T4 e ver o que pode ser feito com o mesmo.

Estrutura de um template

Vamos entrar já na criação de um template simples para conhecer a sintaxe. Primeiro passo é adicionar um template ao projecto: basta adicionar um ficheiro (de texto por exemplo) com a extensão .tt. Para este caso, o HelloWorld.tt parece-se apropriado. Note que, ao ser adicionado o ficheiro ao projecto, um ficheiro novo, HelloWorld.cs, é adicionado. É o template a entrar em acção. Obviamente, porque não fizemos mais nada, o ficheiro de resultado estará vazio.

Sempre que gravamos um ficheiro .tt, o Visual Studio transforma-o automaticamente. Se tivermos vários templates e quisermos transformar todos, de uma só passagem, devemos pressionar o botão no topo da solução “Transform All Templates”.

Dica: Se tiver o T4 editor (menciono-o mais à frente) há itens novos adicionados à lista de itens que podem ser usados num projecto. Um deles é o T4 template que cria um ficheiro com a extensão .t4, que não é um template transformado imediatamente. No entanto, se o escolher e alterar a extensão para .tt, as directivas iniciais são introduzidas automaticamente.

Sintaxe

Os ficheiros t4 devem iniciar com directivas próprias da linguagem e que serão utilizadas para preparar o motor de processamento.

<#@ Template Language="C#v3.5" #>
<#@ Import Namespace="System" #>

Qualquer bloco executável num template começa com <# e termina com o #>, similar ao <% %> do ASP.Net. As directivas tem o @ extra na abertura do bloco. Há 6 directivas pré-definidas:

DirectivaUtilização
<#@ Template … #>Directiva principal que define a linguagem usada no template. Aceita C#, C#v3.5, VB ou VBv3.5.
<#@ Import … #>Permite importar namespaces para utilizar as classes desses espaços no código do template. O System é a base, e praticamente sempre presente.
<#@ Include … #>Permite importar ficheiros para dentro do template que podem ter blocos de código ou funções reaproveitáveis.
<#@ Assembly … #>Permite importar assemblagens a partir do nome ou caminho físico do ficheiro, equivalente a incluir a referência da assemblagem num projecto .NET.
<#@ Output … #>Permite alterar as definições do ficheiro resultante, nomeadamente a extensão do ficheiro e a codificação. Extensões como o .cs e .vb entram no ciclo de compilação do projecto em que está inserido.
<#@ Property …#>Define uma propriedade a usar durante o processamento da transformação.

Com isto, podemos ver que o código que vou utilizar no script é o C# na versão 3.5 do .NET, e que é importado o namespace System para o template, para poder usar os objectos de base do .Net. Por omissão, o ficheiro gerado é da extensão da linguagem preferida no IDE (no meu caso .cs). Para altera, por exemplo, para .html, basta adicionar a directiva de output:

<#@ Output Extension=".html" #>

Vamos então adicionar a informação que queremos incluir no ficheiro resultante. Temos dois tipos de de blocos informação que podemos incluir – texto que é escrito directamente, ou blocos de código que serão processados e que resultam em texto. Por exemplo, se no meu template eu incluir:

<#@ Template Language="C#v3.5" #>
<#@ Import Namespace="System" #>
<#@ Output Extension=".html" #>
<html>
   <body>
      Olá Mundo!
   <body>
</html>

O ficheiro resultante terá escrito:

<html>
   <body>
      Olá Mundo!
   <body>
</html>

Mas o poder dos templates surge quando incluímos blocos de código e dados dinâmicos, como por exemplo:

<#@ Template Language="C#v3.5" #>
<#@ Import Namespace="System" #>
<#@ Output Extension=".html" #>
<html>
   <body>
      Olá Mundo! Fui criado às <#= DateTime.Now.ToString("HH:mm") #>
   <body>
</html>

que resulta em

<html>
   <body>
      Olá Mundo! Fui criado às 20:15
   <body>
</html>

Temos assim um exemplo de um timestamp num ficheiro gerado automaticamente. Este exemplo apresenta um novo bloco de código. Há 4 tipos de blocos:

tipodescrição
<#Bloco de código na linguagem .NET definida e que é inserida directamente no método TransformText() da classe de transformação. Podemos englobar sequências de instruções, condições e ciclos de controlo dentro de blocos deste género, e ainda forçar a escrita de informação no ficheiro de saída com os métodos Write() e WriteLine().
<#=Escreve o resultado de uma expressão, similar a um databind no ASP.NET
<#+Adiciona “class features” ao código, permitindo criar métodos e classes reutilizáveis nos templates
<#@Directivas que apresentam instruções de preparação do motor de processamento

Um gerador de código simples

Vimos as bases. A nível de sintaxe não há muito mais para ver. Resta ver o potencial de transformação do template em código. Automatizar o processo é essencial, especialmente em estruturas complexas. Depois de gerar algumas aplicações (ou mesmo no decorrer de um projecto concreto), há sempre padrões de construção que encontramos e que tentamos tornar mais coerente efectuando um refactoring para maximizar o reaproveitamento de código e a organização dos objectos segundo padrões de POO.

Mesmo assim, há estruturas que ultrapassam a estrutura de objectos, como nomes, tipos de dados, descrições, mapeamento de campos, etc. Um domínio especifico pode ter uma forma coerente de construir objectos com estes parâmetros, e um gerador de código pode ser usado para criar o código do domínio automaticamente. Imagina uma aplicação de n-camadas, com UI, Camada de negócios, Camada de acesso a dados, e a própria estrutura de armazenamento de dados. Tem regras próprias, que em alguns casos podem ser generalizadas, mas as características dos objectos do domínio não são genéricas. Com o gerador de código podemos cria-los e a funcionalidade associada automaticamente!

Vou então pegar neste exemplo, simplificado, para demonstrar o potencial do gerador. Vou considerar a seguinte estrutura:

  • A minha base de dados é um conjunto de tabelas com colunas simples (vou omitir relações pela simplicidade e podermos concentrar no T4).
  • A minha aplicação tem como Business Objects (BO) classes com propriedades, que reflectem as tabelas da base de dados.
  • Para aceder à base de dados, uso um conjunto de classes que vou denominar DAL (Data Access Layer), que executam comandos CRUD sobre a BD, e preenchem o objecto.
  • A UI utiliza os BO para apresentar os dados e os CRUD da DAL para interagir com a BD (estou a omitir uma camada de negócio para simplificar).

Com o gerador de código vou tentar criar o SQL de criação da base de dados, alguns métodos da DAL, e as classes BO.

Definir o suporte da meta-informação

Para gerar o código, preciso de uma estrutura de metadados que descreva o meu domínio. Lembrando que os BO no nosso caso reflectem directamente as tabelas da base, podemos considerar que as classes equivalem ás tabelas e as propriedades às colunas. Uma classe é essencialmente uma lista de propriedades tal como uma tabela é uma lista de colunas.

Posto isto, sei que:

  • classes e tabelas tem o mesmo nome
  • propriedades reflectem colunas
    • propriedades e colunas tem nomes
    • uma coluna pode ser uma chave primaria da tabela
    • uma propriedade pode ter um valor por defeito
    • o tipo de propriedade e o tipo de coluna devem ser equivalentes

A simplicidade permite que eu defina esta estrutura num objecto que posso usar em código.

Vou então definir isto num ficheiro .t4 que adiciono ao meu projecto. Podia efectua-lo num ficheiro .tt, mas este iria gerar um ficheiro vazio extra desnecessário: Classes.t4

<#@ Template Language="C#v3.5" #>
<#@ Import Namespace="System"  #>
<#@ Import Namespace="System.Collections.Generic" #>
<#+
 
public class MinhaPropriedade
{
    public string Nome {get; set;}
    public bool IsPK { get; set; }
    public string TipoCS { get; set; }
    public string TipoDB { get; set; }
    public string ValorDefeito { get; set; }
 
    public MinhaPropriedade(string nome, bool isPK, string tipocs, string tipodb, string valordefeito)
    {
        this.Nome = nome;
        this.IsPK = isPK;
        this.TipoCS = tipocs;
        this.TipoDB = tipodb;
        this.ValorDefeito = valordefeito;
    }
}
 
public class MinhaClasse
{
    public string Nome { get; set; }
    public List<MinhaPropriedade> Propriedades { get; set; }
 
    public MinhaClasse(string nome)
    {
        this.Nome = nome;
        this.Propriedades = new List<MinhaPropriedade>();
    }
}
 
#>

O ficheiro começa com as directivas do template e imports necessários. Depois temos o inicio de bloco <#+ com que podemos definir estruturas de classes. Como vamos incluir este ficheiro noutros ficheiros de templates, as classes aqui definidas serão disponibilizadas nesses ficheiros. Basta incluir o a referência a este ficheiro (como veremos a seguir) para poder usar as classes aqui definidas. O código apresentado no bloco é de simples definição de classe e propriedades.

Assim sendo tenho duas classes disponíveis: MinhaClasse em que defino uma class/tabela e MinhaPropriedade que define propriedades/colunas. Vou querer criar mais uma classe que vai servir para carregar um objecto para gerar código a partir dele. Em casos reais, as estruturas serão mais complexas e portanto estruturas XML ou DSLs completas (que o VS consegue-se criar usando os SDKs disponíveis) ou bases de dados serão usadas para suportar os metadados. Para este caso vamos simplificar e carregar os dados directamente (Loader.t4).

<#@ Template Language="C#v3.5" #>
<#@ Import Namespace="System" #>
<# //@ Include File="Classes.t4" #>
<#+
 
       public class Loader
       {
           public static MinhaClasse CarregarDefinicao()
           {
               MinhaClasse minhaClasse = new MinhaClasse("ObjectoSimples");
               minhaClasse.Propriedades.Add(new MinhaPropriedade("id", true, "int", "int", null));
               minhaClasse.Propriedades.Add(new MinhaPropriedade("nome", false, "string", "varchar(100)", "String.Empty"));
               minhaClasse.Propriedades.Add(new MinhaPropriedade("funcao", false, "string", "text", "String.Empty"));
 
               return minhaClasse;
           }
       }
 
#>

Nota: Novamente, há as directivas bases, e uma que permite incluir o Classes.t4. Durante a escrita da classe, é útil incluir esta directiva para facilitar a escrita, mas posteriormente é necessário comentar para evitar qualquer duplicação das classes de Classe.t4 nos templates. Também de notar, o Include refere um caminho relativo ao ficheiro actual.

Neste ficheiros definimos uma classe Loader com um único método estático que retorna os dados da classe que vamos usar. Novamente, a classe está definida entre marcadores do tipo <#+ #>. Numa DSL (Domain Specific Language) criada no VS, a definição da mesma cria um processador que carrega a estrutura a partir de XML. Podíamos também definir uma estrutura do género em XML ou até em ficheiro texto bem definido, e métodos específicos para o carregar.