http://michael.carbenay.info

BuildProviders

06 avr. 2006  –  asp.net  –  0 Commentaires

Cela faisait un petit moment déjà que j'avais envie de parler de toutes ces fonctionnalités qui font de asp.net une vraie merveille pour le developpeur. Si la version 1.0 (et 1.1) présentait déjà de nombreux interêts, la v2.0 apporte son nombre de nouveautés et d'améliorations, et c'est celles-ci que je vais essayer de vous présenter.

Premier post, premier outil incroyable : les BuildProviders. "Kezako ?", en voila une bonne question ! Eh bien les buildProviders c'est la possibilité de compiler certains fichiers d'une application web en classes. Pourquoi, par exemple, ne pas compiler ce fameux fichier de workflow que vous vous embetez à parser, ou encore ce template de document ? La classe qui sera construite dans ce post fournira une fonctionnalité très banale: un gestionnaire de connexion à une base de données, c'est la façon de le réaliser qui sera un peu différente.

Le but du jeu est donc de transformer en classe le fichier xml suivant:

<Connections>
  <Connection
     id="MaConnection"
     dataSource="(local)"
     initialCatalog="MaBase"
     useSSPI="yes" />
</Connections>

Pour cela, nous allons réaliser une classe qui dérive de System.Web.Compilation.BuildProvider :

public class CnxCompiler : BuildProvider
{
}

Il n'y a pas des beaucoup de méthodes/propriétés à implémenter pour obtenir une classe qui fonctionne. La première chose à faire est de surcharger la propriété CodeCompilerType qui permet au module de préciser quel language et quelles options de compilation (entendez principalement : quelles assemblies doivent être référencées) sont utilisées. Pour notre exemple, il s'agit simplement d'utiliser le compilateur C# par défaut :

    private CompilerType _compilerType = null;
    public CnxCompiler()
    {
        _compilerType = GetDefaultCompilerTypeForLanguage("C#");
    }

    public override CompilerType CodeCompilerType
    {
        get
        {
            return _compilerType;
        }
    }

Viens ensuite le gros du travail : réaliser l'ecriture de la classe. Ce n'est pas très compliqué, mais c'est évidemment un peu verbeux... Pour réaliser cette classe, il existe deux possibilités :

  • soit fournir du code C# "en brut" qui sera compilé par la plateforme (c'est ce que nous allons faire : c'est de TRES loin le plus simple)
  • soit utiliser le CodeDOM
    public override void GenerateCode(AssemblyBuilder assemblyBuilder)
    {
        XmlDocument doc = new XmlDocument();
        using (Stream st = this.OpenStream())
            doc.Load(st);

        // CreateCodeFile permet de générer le code
        // sous la forme d'un pseudo fichier de sources
        TextWriter tw = assemblyBuilder.CreateCodeFile(this);

        // il ne reste plus qu'a générer le code !
        tw.WriteLine("using System;");
        tw.WriteLine("using System.Data;");
        tw.WriteLine("using System.Data.SqlClient;");

        tw.WriteLine("public static partial class ConnectionManager {");
        foreach (XmlElement elm in doc.SelectNodes("/Connections/Connection"))
        {
            // ComputeSafeName ne fait que transformer un nom
            // quelconque en identifiant C# valide (retrait des 
            // caracteres spéciaux, changement de casse, etc.)
            string id = ComputeSafeName(elm.GetAttribute("id"));
            tw.Write(" public static SqlConnection ");
            tw.Write(id);
            tw.WriteLine(" {");
            tw.WriteLine("  get {");
            try
            {
                // BoolConvert converti "true", "yes", "1" en true
                bool useSSPI = BoolConvert(elm.GetAttribute("useSSPI"));
                string username = elm.GetAttribute("username");
                string password = elm.GetAttribute("password");
                string catalog = elm.GetAttribute("initialCatalog");
                string datasource = elm.GetAttribute("dataSource");
                if (string.IsNullOrEmpty(datasource)
                    || string.IsNullOrEmpty(catalog))
                {
                    tw.Write("throw new ApplicationException(");
                    tw.WriteLine("\"connexion invalide\");");
                }
                if (!useSSPI && string.IsNullOrEmpty(username))
                {
                    tw.Write("throw new ApplicationException(");
                    tw.WriteLine("\"pas d'authentification\");");
                }

                // la chaine de connexion
                StringBuilder blrCnString = new StringBuilder();
                blrCnString.Append("Data Source=");
                blrCnString.Append(datasource);
                blrCnString.Append(";Initial Catalog=");
                blrCnString.Append(catalog);
                if (useSSPI)
                {
                    blrCnString.Append(";Integrated Security=SSPI");
                }
                else
                {
                    blrCnString.Append(";User Id=");
                    blrCnString.Append(username);
                    blrCnString.Append(";password=");
                    blrCnString.Append(password);
                }

                tw.WriteLine("SqlConnection cn;");
                tw.Write("cn = new SqlConnection(\"");
                tw.Write(blrCnString);
                tw.WriteLine("\");");
                tw.WriteLine("cn.Open();");
                tw.WriteLine("return cn;");
            }
            catch
            {
            }
            finally
            {
                tw.WriteLine("  }");
                tw.WriteLine(" }");
            }
        }
        tw.WriteLine("}");
        tw.Close();
    }

Bon, le code n'est pas exceptionnel, mais il fait son boulot : pour chaque paramètre de connexion dans le fichier, on génère une nouvelle propriété qui se connecte à la base. C'est un peu bourrin et demande à ce que l'appelant ferme les connexions lui-même, mais c'est pour la beauté de l'exemple, pas pour avoir un framework bullet-proof. A noter l'utilisation du mot clef partial dans la définition de la classe qui permettra d'avoir plusieurs fichiers de paramètres et de tout compiler en une seule classe.

Il faut ensuite surcharger GetGeneratedType pour spécifier quelle est la classe que nous venons de réaliser :

    public override Type GetGeneratedType(CompilerResults results)
    {
        return results.CompiledAssembly.GetType("ConnectionManager");
    }

Voila, le build provider est réalisé, reste à dire à asp.net que nous avons une nouvelle classe de compilation, pour cela nous allons éditer le fichier web.config. Il suffit d'ajouter un noeud buildProviders dans la section system.web/compilation :

<system.web>
    <compilation debug="true">
      <buildProviders>
        <add extension=".cntx" 
             type="BuildProviders.CnxCompiler, BuildProviders" />
      </buildProviders>
      </compilation>
</system.web>

A noter que les build-providers travaillent sur une extension de fichier, pas sur un nom de fichier. Ici nous avons donc défini un nouveau "compilateur" pour les fichiers .cntx. Pour que tout fonctionne bien, vous devrez placer ces fichiers dans le dossier App_Code.

Le plus beau dans l'affaire ? c'est qu'après redémarrage de VS (ou en vidant le cache de Intellisense), la classe ConnectionManager est dispo pour la "complétition de code" (c'est comme ça que l'on dit ?) :

C'est quand même la classe ce truc non ?

Technorati: ,

Ajouter un commentaire


(Affichera votre icône Gravatar)

biuquote
  • Commentaire
  • Aperçu immédiat
Loading