Desenvolvimento - ASP. NET

ASP.NET: Criando um BuildProvider

Neste artigo veremos como os build providers fornecem uma grande funcionalidade que, dependendo do cenário, pode reduzir consideravelmente a quantidade de código manual a ser escrita.

por Israel Aéce



function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i Apesar de você não perceber, muito provavelmente já deve ter utilizado esta funcionalidade. Quando criamos arquivos de resources na nossa aplicação, automaticamente é criada uma classe chamada Resources e, dentro dela, são criadas propriedades que servem de wrapper para acessar cada um dos itens de forma mais simples e, conseqüentemente, já tendo suporte através do intellisense do Visual Studio .NET 2005. Isso é possível graças a um builder provider chamado ResourcesBuildProvider que está contido no namespace System.Web.Compilation.

Os build providers, quando adicionados no sistema de compilação do ASP.NET, nos permite customizar a compilação de alguns tipos de arquivos, como é o caso de arquivos com extensão *.resx. Sendo assim, é possível gerar uma classe dinamicamente, baseando-se em alguma regra e já podendo acessar seus membros (métodos e propriedades) durante o desenvolvimento da aplicação. Além deste exemplo, já há vários builders que a própria Microsoft criou. O trecho de código abaixo foi extraído do arquivo Web.Config global, que está no seguinte endereço: %windir%\Microsoft.NET\Framework\v2.0.50727\CONFIG\:

<buildProviders>
 <add extension=".aspx" type="System.Web.Compilation.PageBuildProvider" />
 <add extension=".ascx" type="System.Web.Compilation.UserControlBuildProvider" />
 <add extension=".master" type="System.Web.Compilation.MasterPageBuildProvider" />
 <add extension=".asmx" type="System.Web.Compilation.WebServiceBuildProvider" />
 <add extension=".ashx" type="System.Web.Compilation.WebHandlerBuildProvider" />
 <add extension=".soap" type="System.Web.Compilation.WebServiceBuildProvider" />
 <add extension=".resx" type="System.Web.Compilation.ResXBuildProvider" />
 <add extension=".resources" type="System.Web.Compilation.ResourcesBuildProvider" />
 <add extension=".wsdl" type="System.Web.Compilation.WsdlBuildProvider" />
 <add extension=".xsd" type="System.Web.Compilation.XsdBuildProvider" />
 <add extension=".js" type="System.Web.Compilation.ForceCopyBuildProvider" />
 <add extension=".lic" type="System.Web.Compilation.IgnoreFileBuildProvider" />
 <add extension=".licx" type="System.Web.Compilation.IgnoreFileBuildProvider" />
 <add extension=".exclude" type="System.Web.Compilation.IgnoreFileBuildProvider" />
 <add extension=".refresh" type="System.Web.Compilation.IgnoreFileBuildProvider" />
 <add 
  extension=".svc" 
  type="System.ServiceModel.Activation.ServiceBuildProvider, System.ServiceModel" />
</buildProviders>
Web.Config Global

O interessante é que esta funcionalidade torna as coisas bem flexíveis e nos permite criar os nossos próprios buider providers. Para o exemplo irei utilizar um cenário bastante comum nas aplicações que desenvolvo. Para todos os valores que coloco na seção AppSettings do arquivo Web.Config da aplicação, eu costumo criar uma classe chamada Settings com propriedades de somente leitura que expõem cada um daqueles itens. Sendo assim, a idéia aqui será criar uma classe contendo propriedades que nada mais serão do que wrappers para cada um dos valores definidos na seção AppSettings.

Um detalhe importante antes de começarmos a estudar a geração do código é mencionar que tal geração deverá ser feita utilizando CodeDom (namespace System.CodeDom), que fornece tipos que representam elementos de código. Para maiores informações sobre este assunto, consulte este endereço.

O primeiro passo é criar a classe que será responsável por essa geração, que será o nosso build provider. Obrigatoriamente precisamos herdar da classe base chamada BuildProvider que está dentro do namespace System.Web.Compilation. Essa classe fornece um método chamado GenerateCode (que deverá ser sobrescrito em nosso build provider) e, como parâmetro, passa a este método uma instância do tipo AssemblyBuilder que referenciará o código gerado pelo build provider. É neste momento que precisamos recorrer ao CodeDom para gerar o código da classe proposta. Os passos são:

  • Criação de uma namespace chamado Config para organizar melhor o código a ser gerado;

  • Criar uma classe chamada Settings que armazenará as propriedades. Essa classe precisa estar dentro do namespace Config e, além disso, é necessária que seja partial para permitir que o consumidor possa "estendê-la";

  • Finalmente, para cada item adicionado na seção AppSettings do arquivo Web.Config da aplicação, é necessário criar uma propriedade e adicionar dentro da classe Settings. Essas propriedades devem ter como nome a chave e o que ela retornará será o valor de cada item do AppSettings.

Tendo esses objetivos, vamos escrever o código que efetua essa criação. O código abaixo exibe, na íntegra, o build provider já totalmente criado.

using System;
using System.CodeDom;
using System.Configuration;
using Web = System.Web.Compilation;

public class SettingsBuildProvider : Web.BuildProvider
{
    public override void GenerateCode(Web.AssemblyBuilder assemblyBuilder)
    {
        CodeNamespace ns = new CodeNamespace("Config");
        CodeTypeDeclaration settingsClass = new CodeTypeDeclaration("Settings");
        settingsClass.IsPartial = true;
        this.GenerateProperties(settingsClass);
        ns.Types.Add(settingsClass);
        ns.Imports.Add(new CodeNamespaceImport("System.Configuration"));

        CodeCompileUnit unit = new CodeCompileUnit();
        unit.Namespaces.Add(ns);
        assemblyBuilder.AddCodeCompileUnit(this, unit);
    }

    private void GenerateProperties(CodeTypeDeclaration type)
    {
        foreach (string key in ConfigurationManager.AppSettings.AllKeys)
        {
            CodeMemberProperty prop = new CodeMemberProperty();
            prop.Name = key;
            prop.Type = new CodeTypeReference(typeof(string));
            prop.Attributes = MemberAttributes.Public | MemberAttributes.Static;
            prop.HasSet = false;

            prop.GetStatements.Add(
                new CodeMethodReturnStatement(
                    new CodeSnippetExpression(
                        string.Format(
                            "ConfigurationManager.AppSettings[\"{0}\"]", 
                            key))));

            type.Members.Add(prop);
        }
    }
}
Imports System
Imports System.CodeDom
Imports System.Configuration
Imports Web = System.Web.Compilation

Public Class SettingsBuildProvider
    Inherits Web.BuildProvider

    Public Overrides Sub GenerateCode(ByVal assemblyBuilder As Web.AssemblyBuilder)
        Dim ns As New CodeNamespace("Config")
        Dim settingsClass As New CodeTypeDeclaration("Settings")
        settingsClass.IsPartial = True
        Me.GenerateProperties(settingsClass)
        ns.Types.Add(settingsClass)
        ns.Imports.Add(New CodeNamespaceImport("System.Configuration"))

        Dim unit As New CodeCompileUnit()
        unit.Namespaces.Add(ns)
        assemblyBuilder.AddCodeCompileUnit(Me, unit)
    End Sub

    Private Sub GenerateProperties(ByVal type As CodeTypeDeclaration)
        For Each key As String In ConfigurationManager.AppSettings.AllKeys
            Dim prop As New CodeMemberProperty()
            prop.Name = key
            prop.Type = New CodeTypeReference(GetType(String))
            prop.Attributes = MemberAttributes.Public Or MemberAttributes.Static
            prop.HasSet = False

            prop.GetStatements.Add( _
                New CodeMethodReturnStatement( _
                    New CodeSnippetExpression( _
                        String.Format( _
                            "ConfigurationManager.AppSettings[""{0}""]", 
                            key))))

            type.Members.Add(prop)
        Next
    End Sub
End Class
C# VB.NET

Analisando o código acima, dentro do método GenerateCode inicialmente criamos o namespace padrão onde a classe de configuração irá residir. A criação do namespace é realizada através do objeto CodeNamespace que, em seu construtor, podemos informar o nome do mesmo; em seguida, utilizando uma instância da classe CodeTypeDeclaration, criaremos a classe que irá expor as propriedades do arquivo Web.Config. A classe CodeTypeDeclaration fornece uma propriedade chamada IsPartial que, opcionalmente, vamos definir para true.

Com a classe devidamente criada, o próximo passo é percorrer a coleção de itens da seção AppSettings e, para cada um deles, criar uma propriedade. Esse passo é realizado pelo método privado chamado GenerateProperties. Como essas propriedades expõem valores estáticos, as mesmas também precisam ser criadas como propriedades estáticas. Para criar a propriedade, utilizamos uma instância da classe CodeMemberProperty, definimos o nome, tipo e através da propriedade GetStatements, especificamos ali o que ela retornará. Finalmente, adicionamos essa propriedade na coleção de membros do tipo (classe) que é passado como parâmetro para este método.

Para finalizar a explicação da geração do código, adicionamos a classe já completamente configurada na coleção de tipos do namespace Config que criamos anteriormente. Como essa classe dinâmica referencia a classe ConfigurationManager, é necessário importarmos o namespace System.Configuration. para dentro do namespace Config que criamos. Depois disso, criamos um objeto do tipo CodeCompileUnit, que representa um container para o CodeDom e, através da propriedade Namespaces, adicionamos o namespace que criamos. Por fim, invocamos o método AddCodeCompileUnit, passando o build provider corrente e o container de código gerado.

Uma vez que o build provider está criado, é necessário registrar o mesmo dentro da aplicação Web que o utilizará. Para isso, o ASP.NET fornece um elemento chamando buildProviders (contido dentro do elemento compilation), onde informamos o tipo (type) referente à classe geradora do código e uma extensão (extension), onde a mesma irá depositar o código gerado. Para que isso funcione bem, é necessário que dentro do diretório App_Code exista um arquivo "dummy" com a mesma extensão definida nesta seção. Como as informações a serem utilizadas para a geração do código serão extraídas do arquivo Web.Config, este arquivo "dummy" não necessita ter nenhum dado dentro. O trecho de código abaixo exemplifica o registro do build provider:

<?xml version="1.0"?>
<configuration>
  <appSettings>
    <add key="EmailParaNotificacao" value="israel@projetando.net"/>
    <add key="SiteUrl" value="http://www.projetando.net/"/>
    <add key="BlogUrl" value="http://weblogs.pontonetpt.com/israelaece/"/>
  </appSettings>
  <system.web>
    <compilation debug="true">
      <buildProviders>
        <add
          extension=".sett"
          type="BuildProvider.Library.SettingsBuildProvider, BuildProvider.Library"/>
      </buildProviders>
    </compilation>
  </system.web>
</configuration>
Web.Config

Como o cenário é gerar código (propriedades), para cada um dos itens que estão na seção AppSettings do Web.Config, ali os mesmos já estão definidos. Com esse registro feito, agora já temos suporte em tempo de desenvolvimento, permitindo que o intellisense do Visual Studio .NET 2005 já mostre as propriedades criadas. A imagem abaixo ilustra esse funcionamento:

Figura 1 - Intellisense já suportando as propriedades criadas dinamicamente.

O interessante é que, se você colocar um Breakpoint na linha em que faz a chamada para algumas destas propriedades, ao entrar dentro da mesma, você enxergará a classe que foi criada, com a seguinte mensagem:

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.832
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace Config {
    using System.Configuration;
    
    
    public partial class Settings {
        
        public static string EmailParaNotificacao {
            get {
                return ConfigurationManager.AppSettings["EmailParaNotificacao"];
            }
        }
        
        // outras propriedades...
    }
}
Classe Dinâmica

Conclusão: Como pudemos ver no decorrer deste artigo, os build providers fornecem uma grande funcionalidade que, dependendo do cenário, pode reduzir consideravelmente a quantidade de código manual a ser escrita. O interessante é que você não precisa se limitar o acesso a informações do próprio ASP.NET (como foi o caso do AppSettings que utilizamos aqui). Você pode criar um arquivo qualquer, padrão XML, CSV, etc. e o teu build provider fazer o parser e, conseqüentemente, gerar o código referente para que o desenvolvedor possa acessar as informações ainda em tempo de desenvolvimento.

Faça o download do exemplo.

Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.