Templates T4

Definir os objectos

Vamos gerar então um objecto de negócio BO. No exemplo vou apenas mostrar uma única classe, e portanto o ficheiro de saída será directamente incluído no projecto. Vamos então criar o ObjectoSimples.tt:

<#@ Template Language="C#v3.5" #>
<#@ Import Namespace="System"  #>
<#@ Import Namespace="System.Collections.Generic" #>
<#@ Output Extension=".cs" #>
<#@ Include File="Classes.t4"  #>
<#@ Include File="Loader.t4" #>
<#
      MinhaClasse minhaClasse = Loader.CarregarDefinicao();
#>
using System;
 
public class <#= minhaClasse.Nome #>
{
<#
    foreach(MinhaPropriedade propriedade in minhaClasse.Propriedades)
    {
#>
    private <#= propriedade.TipoCS #> _<#= propriedade.Nome #><#= propriedade.ValorDefeito != null ? " = " + propriedade.ValorDefeito : "" #>;
    public <#= propriedade.TipoCS #> <#= propriedade.Nome #>
    {
        get { return _<#= propriedade.Nome #>; }
        set { _<#= propriedade.Nome #> = value; }
    }
 
<#
    }
#>
}

Temos agora um ficheiro com as directivas necessárias. Para além do template e imports, temos os includes dos .t4 que definimos anteriormente, e ainda indicamos explicitamente que o ficheiro gerado pelo template tem a extensão .cs.

O próximo bloco é do tipo <# #> (sem qualquer carácter especial adicional). Este tipo de bloco é incluído directamente no TransformText() da classe criada a partir do template. Neste caso é instanciado MinhaClasse e carregado os dados da classe através da chamada a Loader.CarregarDefinicao().

De seguida, é escrito texto directamente para o ficheiro de saída, que neste caso será o using e o início da declaração da classe. O nome da classe é incluído usando <#= minhaClasse.Nome #>, onde acedemos directamente à propriedade Nome da instância minhaClasse que criámos. Após a declaração da classe, listamos as propriedades na mesma, usando um ciclo foreach num conjunto de blocos <# #>.

using System;
 
public class ObjectoSimples
{
    private int _id;
    public int id
    {
        get { return _id; }
        set { _id = value; }
    }
 
    private string _nome = String.Empty;
    public string nome
    {
        get { return _nome; }
        set { _nome = value; }
    }
 
    private string _funcao = String.Empty;
    public string funcao
    {
        get { return _funcao; }
        set { _funcao = value; }
    }
 
}

O template gerou assim o código do objecto com os campos privados e as propriedades públicas, getters e setters incluídos. Podia ainda incluir atributos de serialização, comentários de descrição, etc. Quaisquer dados necessários devem ser colocados nos metadados e usados pelo template para incluir no código com a formatação desejada.

Definir script da base de dados

Vamos gerar SQL agora, para criar a tabela na base de dados (ObjectoSimples.tt):

<#@ Template Language="C#v3.5" #>
<#@ Import Namespace="System"  #>
<#@ Import Namespace="System.Collections.Generic" #>
<#@ Output Extension=".sql" #>
<#@ Include File="Loader.t4" #>
<#@ Include File="Classes.t4" #>
<#
      MinhaClasse minhaClasse = Loader.CarregarDefinicao();
#>
CREATE TABLE <#= minhaClasse.Nome #>
{
<#
      for (int i = 0; i < minhaClasse.Propriedades.Count; i++ )
      {
          MinhaPropriedade propriedade = minhaClasse.Propriedades[i];
#>
    <#= propriedade.Nome #> <#= propriedade.TipoDB #> <#= propriedade.IsPK ? "PRIMARY KEY" : "" #><#=    i<minhaClasse.Propriedades.Count-1 ? "," : "" #> 
<#
      }
#>
}

que gera:

CREATE TABLE ObjectoSimples
{
    id INT PRIMARY KEY, 
    nome VARCHAR(100), 
    funcao text  
}

Repara que, a partir da mesma definição, foi possível criar dois ficheiros de código de tipos diferentes, baseados maioritariamente em parâmetros comuns. Neste caso definimos a saída do tipo .sql. Um ficheiro SQL deste tipo teria o script de criação da base, incluindo relações a adicionar, sequências, stored procedures base, etc. O template que gerei foi direccionado para SQL server, mas podia perfeitamente direccionar a outro SGBD ou até criar mais um template dedicado a outra base de dados.

Definir o acesso a dados

Para a DAL, teremos essencialmente uma classe com um conjunto de métodos que implementa métodos CRUD comuns como obter um registo por um ID, uma lista por um conjunto de parâmetros, número de registos por parâmetros, inserção e actualização de registos, actualização determinado campo de um registo ou ainda apagar registos. Deve ainda ser capaz de mapear os dados dos campos da base de dados para as propriedades dos objectos respectivos. No exemplo que segue, demonstro uma versão simples de acesso a um registo e preenchimento de um objecto (ObjectoSimplesDAL.tt):

<#@ Template Language="C#v3.5" #>
<#@ Import Namespace="System"  #>
<#@ Import Namespace="System.Collections.Generic" #>
<#@ Output Extension=".cs" #>
<#@ Include File="Loader.t4" #>
<#@ Include File="Classes.t4" #>
<#
      MinhaClasse minhaClasse = Loader.CarregarDefinicao();
#>
using System;
using ClassesGenericasDeAcessoADados;
 
public class <#= minhaClasse.Nome #>DAL
{
 
        /// <summary>
	/// Retorna um objecto do tipo <#= minhaClasse.Nome #> a partir da chave
	/// </summary>
<#
      MinhaPropriedade chave = null;
      foreach (MinhaPropriedade prop in minhaClasse.Propriedades)
      {
          if (prop.IsPK)
          {
              chave = prop;
              break;
          }
      }
#>
	/// <param name="<#= chave.Nome #>">ID a pesquisar</param>
	/// <returns></returns>
	public <#= minhaClasse.Nome #> GetItem(<#= chave.TipoCS #> <#= chave.Nome.ToLower() #>)
	{
		string sqlQuery = "SELECT * FROM <#= minhaClasse.Nome #> WHERE <#= chave.Nome #> = " + <#= chave.Nome.ToLower() #>;
 
		try
		{
			using(DataAccessor dataAccessor = new DataAccessor(connectionString) )
				return (<#= minhaClasse.Nome #>)dataAccessor.GetDBObject(sqlQuery, FillObject);
                }
                catch(Exception e)
                {
		        //código de logging
                }
	}
 
        /// <summary>
        /// Preenche <#= minhaClasse.Nome #> com os dados provenientes da base
        /// </summary>
        /// <param name="data">IDataReader com os dados recebidos</param>
        /// <returns></returns>
	public static <#= minhaClasse.Nome #> FillObject(IDataReader data)
	{
		<#= minhaClasse.Nome #> obj = new <#= minhaClasse.Nome #>();
 
		using (DataParser dataParser = new DataParser(data))
                {	
<#
    foreach(MinhaPropriedade propriedade in minhaClasse.Propriedades)
    {
#>
			if (dataParser.IsValid<#= propriedade.TipoCS #>("<#= propriedade.Nome #>"))
		        	obj.<#= propriedade.Nome #> = dataParser.DRGet<#= propriedade.TipoCS #>("<#= propriedade.Nome #>");
<#
    }
#>								       
		}
 
		return obj;
	}
}

que resulta em:

using System;
using ClassesGenericasDeAcessoADados;
 
public class ObjectoSimplesDAL
{
 
        /// <summary>
	/// Retorna um objecto do tipo ObjectoSimples a partir da chave
	/// </summary>
	/// <param name="id">ID a pesquisar</param>
	/// <returns></returns>
	public ObjectoSimples GetItem(int id)
	{
		string sqlQuery = "SELECT * FROM ObjectoSimples WHERE id = " + id;
 
		try
		{
			using(DataAccessor dataAccessor = new DataAccessor(connectionString) )
				return (ObjectoSimples)dataAccessor.GetDBObject(sqlQuery, FillObject);
                }
                catch(Exception e)
                {
			//código de logging
                }
	}
 
	/// <summary>
        /// Preenche ObjectoSimples com os dados provenientes da base
        /// </summary>
        /// <param name="data">IDataReader com os dados recebidos</param>
        /// <returns></returns>
	public static ObjectoSimples FillObject(IDataReader data)
	{
		ObjectoSimples obj = new ObjectoSimples();
 
		using (DataParser dataParser = new DataParser(data))
               {	
			if (dataParser.IsValidint("id"))
				obj.id = dataParser.DRGetint("id");
			if (dataParser.IsValidstring("nome"))
				obj.nome = dataParser.DRGetstring("nome");
			if (dataParser.IsValidstring("funcao"))
				obj.funcao = dataParser.DRGetstring("funcao");
 
		}
 
		return obj;
	}	
}

Repare que foi possível uniformizar os comentários XML para documentação, que ficam simples de manter, e ainda uniformizar a estrutura de de acesso a dados. DataAccessor é um objecto com operações genéricas (como o getDBObject) implementadas no namespace ClassesGenericasDeAcessoADados. GetDBObject está integrado nesse objecto, executa uma query na base, e devolve um objecto preenchido através do método delegado FillObject.

FillObject também é criado no template e integrado na classe da DAL. Essencialmente, utiliza uma classe DataParser com alguns métodos de validação e obtenção de dados, preenchendo as propriedade de ObjectoSimples com os dados das colunas respectivas.

Com estas classes já geradas, podíamos perfeitamente compilar os ficheiros num projecto de DLL e incluí-lo em aplicações que pretendemos construir para o domínio dos nossos objectos. Quaisquer alterações ao domínio, evolveria apenas regenerar os ficheiros e recompilar, substituindo a DLL original.

O Processo de Transformação

Até agora, vimos a sintaxe dos templates, como podemos integrar código nos templates, e ainda uma possível aplicação, mesmo que simples e rudimentar. Apesar de bastante simples, o sistema é bastante poderoso. Conseguimos utilizar as diversas classes do .Net directamente, incluir assemblagens com mais funcionalidade, e ainda definir novas estruturas a utilizar e reaproveitar nos templates.

A estrutura de processamento é algo complexo, mas permite flexibilidade no uso das transformações para aumentar as potencialidades, nomeadamente na possibilidade de definir objectos de leitura de inputs e de versões do motor de processamento. Convém entender como é feita a transformação.

Cada template que criamos é transformado numa class que é posteriormente instanciado para efectuar a produção do ficheiro de saída. Analisando o exemplo da data/hora e a classe transformada:

namespace A7eae43e34a4a4ca88be15d4f32fa114c {
	using System;	
 
	public class HelloWorld : Microsoft.VisualStudio.TextTemplating.TextTransformation 
        {
 
		public override string TransformText() {
			try {
				this.Write("rn");
				this.Write("rn");
				this.Write("rn<html>rn   <body>rn      Olá Mundo! Fui criado às ");
                                this.Write(Microsoft.VisualStudio.TextTemplating.ToStringHelper.ToStringWithCulture(DateTime.Now.ToString("HH:mm")));
				this.Write("rn   <body>rn</html>rnrn");
			}
			catch (System.Exception e) 
                        {
				System.CodeDom.Compiler.CompilerError error = new System.CodeDom.Compiler.CompilerError();
				error.ErrorText = e.ToString();
				error.FileName = "HelloWorld.tt";
				this.Errors.Add(error);
			}
			return this.GenerationEnvironment.ToString();
		}
	}
}

Repara que foi gerado um namespace temporário e incluído as referências das directivas. Foi criado uma classe com o nome do nosso ficheiro e que herda de Microsoft.VisualStudio.TextTemplating.TextTransormation. Esta classe tem um método TransformText() ao qual é feito um override. A nova definição do método inclui o texto do nosso template. Repare que o texto escrito fora dos blocos <# #> é inserido em métodos Write(). Texto em blocos de expressões <#= #> é inserido num Write() como valor de retorno do Microsoft.VisualStudio.TextTemplating.ToStringHelper.ToStringWithCulture(). Qualquer código em blocos <# #> é inserido no método TransformText() como código do mesmo. Podemos adicionar mais métodos a esta classe (HelloWorld) usando o controlo <#+ #>, como métodos auxiliares, ou novas classes se as definirmos em blocos <#+ #> de ficheiros externos incluídos pela directiva <#@ Include #>.

Esta classe é, depois de criada, instanciada pelo motor de processamento e o método TransformText() é chamado para criar o ficheiro resultante. O acesso a recursos é controlado por uma entidade conhecida por ‘Host’ que o motor de processamento utiliza sempre que necessita de aceder ao ambiente aplicacional. À partida temos apenas um host incluído na API de extensibilidade do VS.

Com o host original, o template já é bastante funcional e poderoso. Mas é extraordinário quando construímos os nossos próprios hosts e processadores. A possibilidade de o motor de processamento interagir com os nossos Hosts permite-nos definir estruturas de metadados próprias do domínio em que estamos a trabalhar, com validação e ferramentas mais eficientes na produção das mesmas. A mesma estrutura de classes rudimentar que definimos no exemplo pode ser construído e estendido numa DSL e gerado uma ferramenta de trabalho para criar o ficheiro de definições para a mesma. O host gerado alimentará o motor com a informação necessária para construirmos código ou outro tipo de recurso necessário.

Passos em frente

O uso de templates não é limitado à geração de código com o definido anteriormente. Pode ser usado para gerar documentação de um projecto, por exemplo, e que pode seguir em diversos formatos, usando templates diferentes. Pode também ser usado para processar ficheiros de texto, extraído determinados campos ou blocos para outro. Já os utilizei, por exemplo, para criar enumerações e dicionários em C# a partir de uma lista oficial de distritos e concelhos e moradas.

Também não estamos limitados a um ficheiro de saída por template. Apesar de não suportado directamente, há exemplos de métodos a incluir que permite guardar o conjunto de texto gerado até determinado momento num ficheiro. Com um template, é possível percorrer todos os objectos do domínio e criar uma classe BO, DAL, BLL, e documentação para cada um, com cada bloco gerado num ficheiro independente e integrado no projecto. Esta solução e muitas outras estão descritas por Oleg Sych no blog dele, e incluído no T4 Toolkit que ele desenvolveu. O blog deste autor é actualmente, e na minha opinião, a melhor fonte de informação acerca dos templates T4 e o T4 Toolkit e os exemplos apresentados são valiosos para maximizar o aproveitamento.

Uma outra forma de maximizar a potencialidade é construir uma DSL. A Microsoft disponibiliza os Visual Studio DSL Tools para construir DSLs gráficos, integrados no VS, melhorando a experiência de produzir os metadados. O Class Designer já existente no VS é um bom exemplo de uma DSL integrada. Um bom livro a seguir é o Domain Specific Development with Visual Studio DSL Tools. Não é um tema leve, mas as potencialidades justificam o esforço. O blog do Hugo Ribeiro também é uma boa fonte de dados.

Por fim, é importante referir que o Visual Studio tem um “defeito” relacionado com os templates T4. Não inclui suporte de cor e intelisense à sintaxe dos templates. A falta de coloração e intelisense torna a produção dos templates um processo duro. Felizmente há um add-in chamado T4-editor, criado pela Clarius Consulting, que existe na versão profissional e comunitária (gratuita mas com algumas limitações). A versão profissional é bastante em conta (cerca de 90 euros) e justifica o custo facilmente. No mínimo, justifica integrar a versão comunitária para melhorar a experiência.

Bibliografia e ligações externas