• Conseils pour développeurs (asp.net) du dimanche

    Cela fait quelques semaines que j'interviens de façon ponctuelle sur la maintenance d'un extranet pour un client, et que je m'énerve régulièrement sur l'incapacité de la plupart des développeurs à comprendre qu'un code doit être un minimum propre... L'application, écrite en asp.net, est un ensemble de morceaux de code réalisés, probablement, à la va-vite par des CDD si j'en juge par la "qualité" très médiocre.

    Voici donc quelque conseils à retenir si vous faites du développement ASP.net et que vous souhaitez voir votre code être maintenu...

    N'accèdez jamais, directement dans votre page, à des variables de sessions ou d'application : c'est probablement le B-A-BA du développement avec des langages typés... Il n'est pas normal de voir dans une page ASP.net (sauf cas exceptionnels) des accès du type :

    (int) Session["MEMBRE_ID"] // non !!!!!

    Pourquoi cela n'est-il pas bien ? eh bien tout simplement parce que si une autre page accède (ou encore plus catastrophique met à jour) votre valeur, il y a 1 chance sur 2 pour que celle-ci ne manipule pas le même type de données (dans l'exemple de l'application extranet, l'une des pages traitait l'identifiant client comme un decimal et l'autre comme un int...). Pour réaliser un code maintenable, il vous reste à faire une class "Helper" qui se charge de masquer l'accès à la variable de session et le typage :

    public class SessionHelper
    {
        public int MembreId
        {
            get
            {
                object o = Session["MEMBRE_ID"];
                if (o == null || !(o is int))
                    return 0;
                return (int)o;
            }
            set
            {
                Session["MEMBRE_ID"] = value;
            }
        }
    }

    Avec ça, plus jamais de "InvalidCastException" - enfin, si je ne me suis pas trompé dans l'écriture de la classe :) - et vous risquez moins de problème qu'à tout stocker en string (je pense principalement à des soucis d'injection sql...). Sans parler du fait que vous ne chercherez plus si vous avez appelé votre variable "MEMBRE_ID", "IdMembre", "MEMBREID", ou tout autre variation...

    Dans le même ordre d'idée, ne réalisez jamais les updates sans typer vos variables : à moins de réaliser une application poubelle et donc d'utiliser le RAD à 100%, vous avez probablement écrit des méthodes (si ce n'est des objets) pour l'implémentation de vos règles métiers et de vos accès aux données. Bien ! mais pitié, n'utilisez pas des signatures du type :

    public static Membre GetMembre(string membreId)
    {
         // non !!!!!
    }
    

    lorsque vous savez que membreId est un int, cela fait désordre. Lorsque je vois ce genre de signature, cela m'effraie toujours un peu : si le développeur n'a même pas été capable de typer ses variables, il y a peu de chance qu'il ai fait des requêtes paramétrées, et c'est donc une porte ouverte à l'injection SQL...

    Si vous avez des cas complexes dans vos règles de gestion, découpez le problème en différentes méthodes axées chacune sur la résolution d'un problème simple. Ce conseil la, je pensais vraiment que tout le monde l'avait en tête, mais je me trompais lourdement... Toujours sur le même exemple (oui, cela pourrait être un cas d'école cet extranet, pour le cours "programmez comme des pieds, c'est mieux !"), l'application présente une menu général dont les fonctionnalités changent en fonction de critères multiples (droits accordé à un client, type de structure dont il fait partie, etc.), voici (extrêmement simplifiée) la façon dont cela est traité :

    string matricePossibilite = "";
    matricePossiblite += Session["MEMBRE_TYPE"].ToString();
    matricePossibilite += Session["..."].ToString();
    // il y a encore 3 autres criteres comme ceci...
    
    switch (matricePossibilite)
    {
        case "11012":
            // une bonne cinquentaine de lignes de code
            break;
    
        case "21012":
            // d'autres lignes...
            break;
    }

    Non, non, vous ne rêvez pas, c'est bel est bien écrit comme cela, avec un minimum de commentaires, pour la plupart inutiles d'ailleurs, histoire de simplifier l'affaire... Hormis le fait que tout le code se trouve dans la même méthode, c'est aussi typiquement le cas ou un switch est une très mauvaise idée : cela rends plus difficile à lire. Admettons que l'on garde le switch pour une premiere ré-écriture, il est déjà plus qu'obligatoire de virer toutes ces lignes de codes :

    string matricePossibilite = "";
    matricePossibilite += Session["MEMBRE_TYPE"].ToString();
    matricePossibilite += Session["..."].ToString();
    // il y a encore 3 autres critères comme ceci...
    
    switch (matricePossibilite)
    {
        case "11012":
            NomDuCasNumero1();
            break;
    
        case "21012":
            NomDuCasNumero2();
            break;
    }
    

    C'est déjà un poil plus lisible... Il reste ensuite, soit à bien documenter ce que signifie chacun des cas :

    case "11012":
    // l'utilisateur est de type ...
    // dans une structure ....
    // etc

    Ou même encore mieux : ne pas utiliser de variable bizarre pour déterminer les droits mais des variables avec des noms compréhensibles :

    bool estAdmin = SessionHelper.MembreType == MembreType.Admin;
    TypeStruct type = SessionHelper.TypeStructure;
    // les autres critères selon le même principe
    
    if(estAdmin)
    {
        if(...) // un autre critère
            NomDuCasNumero1();
        else
            NomDuCasNumero2();
    }
    else if(!estAdmin && type==TypeStruct.Type1)
    {
        NomDuCasNumero3();
    }
    // etc.
    

    C'est un peu plus verbeux, et cela demandera certainement un peu de réflexion pour ne pas tomber dans une liste de if/else if ou une cascade de if imbriqués - pensez de nouveau à découper en plusieurs méthodes si vous avez trop de if... - mais qu'est-ce que c'est plus simple à comprendre !

    Si vous êtes adepte des procédure stockées (ce n'est pas mon cas, mais bon, tous les goûts sont dans la nature...), n'hésitez pas à les nommer proprement : mbr_s c'est bien comme nom mais MembreSuppr c'est carrément plus lisible... et tant qu'à faire, pensez à définir des méthodes-metier dont les noms sont en relation avec celui de la proc-stock.

     

    Voila, ce sont les quelques conseils/remarques qui me sont venus à l'esprit au cours des heures passées à me battre avec cette application. Pour résumer :

    • la plupart des langages actuels sont fortement typés (cela n'a pas que des avantages, mais évite certains dérapages...), assurez vous donc que votre code en soit conscient et compense automatiquement les cas où le typage est plus léger (pour asp.net, il faut comprendre par là : tout ce qui à été conservé compatible avec asp...)
    • votre code doit pouvoir être compris dans son ensemble par un autre développeur sans avoir à lire 2000 pages de document Word ou sans avoir besoin de lire dans vos pensées, un peu de commentaires et surtout un code lisible qui peut se comprendre facilement est très souvent préférable à un code plus optimisé - ou du moins qui vous paraît plus optimisé.
    • ah...oui... pour avoir eu aussi un problème de code source livré différents de l'application en production : utilisez un système de gestion de source : il est rageant de devoir décompiler la version de production pour retrouver des bouts de code...

    Et pour thierry, si il passe sur ce billet : essayez de trouver un prestataire qui ne soit pas en carton pour la prochaine fois :)

  • Redirection de dossier par un VirtualPathProvider

    L'un des grands points forts de Asp.net 2.0, c'est la simplicité avec laquelle ont peut étendre le système. Parmis les nombreuses possibilités d'extensions possible, je vous propose de nous servir des HttpModules et des VirtualPathProviders pour effectuer une chose toute simple : la redirection de certains path de votre site web vers d'autres dossiers.

    Imaginons par exemple, que vous ayez un très grand nombre d'images de produits : vous souhaitrez certainement ne pas les stocker directement dans votre application web - les mises à jours devenant vite des galères ingérables dans le cas contraire - mais plutôt dans un autre dossier. Vous avez alors deux possibilités :

    • écrire un HttpHandler qui accèdera au fichier et l'enverra dans la stream de sortie
    • écrire un VirtualPathProvider dont le but est de de "virtualiser" - ah bah, tiens, VirtualPathProvider, Virtualiser, j'l'avais pas vu :) - l'accès aux fichiers.

    Les blogs et forums regorgeant d'exemples pour la premiere solution, c'est l'utilisation d'un VirtualPathProvider qui sera détaillé dans la suite de ce billet.

    Si l'on regarde la signature - simplifiée pour ne présenter que ce qui nous interesse ici - de la classe VirtualPathProvider nous trouvons 4 méthodes très intéressantes :

       public abstract class VirtualPathProvider 
       {
            public virtual bool DirectoryExists(string virtualDir);
            public virtual bool FileExists(string virtualPath);
            public virtual VirtualDirectory GetDirectory(string virtualDir);
            public virtual VirtualFile GetFile(string virtualPath);
       }

    En implementant ces méthodes, nous allons pouvoir rediriger l'accès à un fichier 'virtuel' (disons par exemple ~/Factures/F200601003.pdf) vers un path "reel" (e:\data\legal\factures\200601003.pdf). Pour cela, il est indispensable de surcharger FileExists et GetFile, les méthodes concernant les directory n'étant ici pas très utiles. Voici le code de la classe :

       public class MyVirtualPathProvider : VirtualPathProvider
       {
            private string _virtualDir;
            private string _realDir;
            public MyVirtualPathProvider(string virtualDir, string realDir)
            {
                _realDir = realDir;
                _virtualDir = virtualDir;
            }
            internal string ToRealPath(string virtualPath)
            {
                virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
                if(virtualPath.StartsWith(_virtualDir, 
    StringComparison.InvariantCultureIgnoreCase)) return virtualPath.Replace(_virtualDir, _realDir); return null; } public override bool FileExists(string virtualPath) { string realPath = ToRealPath(virtualPath); if (realPath!=null && File.Exists(realPath)) return true; return Previous.FileExists(virtualPath); } public override VirtualFile GetFile(string virtualPath) { string realPath = ToRealPath(virtualPath); if (realPath!=null && File.Exists(realPath)) return new MyVirtualFile(realPath, virtualPath); return Previous.GetFile(virtualPath); } }

    Comme vous pouvez le constater, celle-ci est assez simple : le constructeur permet de définir le path virtuel à remplacer et le path réel correspondant, la méthode ToRealPath se charge de convertir le path, FileExists retourne simplement true si le fichier reel existe, seule la méthode GetFile est un tout petit peu plus complexe, puisqu'elle fait appel à une autre classe MyVirtualFile. Un certain nombre de points sont à prendre en compte lors de l'écriture de cette classe :

    • Comme nous le verrons d'ici peu, les VirtualPathProvider sont enregistrés globalement pour une application et ne sont pas montés/associés à un path particulier, il convient donc de ne répondre qu'aux demandes pour lesquelles nous avons quelque-chose à dire.
    • Toutes les requêtes qui ne nous concerne pas doivent être passées au VirtualPathProvider suivant -ooops, "précédent" - dans la pile. Celui-ci est disponible par l'intermédiaire de la propriété Previous.
    • Il est aussi nécessaire d'écrire une classe dérivant de VirtualFile  qui se chargera de l'accès effectif au fichier. La seule méthode à surcharger importante de cette classe est la méthode Open qui doit renvoyer une Stream sur les données du fichier.
       public class MyVirtualFile : VirtualFile
       {
            private string _realPath;
            public MyVirtualFile(string realPath, string virtualPath)
                : base(virtualPath)
            {
                _realPath = realPath;
            }
            public override System.IO.Stream Open()
            {
                return File.OpenRead(_realPath);
            }
       }
    

    Pour que tout cela fonctionne, il ne nous reste plus qu'à "enregistrer" un MyVirtualPathProvider auprès du moteur de asp.net. Cela est assez simple, mais doit être fait à certains endroits très précis : il faut qu'il soit enregistré avant toute requête. Pour cela, le plus simple est certainement de le faire au sein de la méthode Application_Start du global.asax

       void Application_Start(object sender, EventArgs e) 
       {
            System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(
                new MyVirtualPathProvider("~/Factures/", "e:\\data\\legal\\factures\\"));
       }

    Et voila ! Maintenant, toutes les requêtes pour un fichier sous le dossier virtuel ~/Factures sera intercepté par notre VirtualPathProvider et le fichier obtenu depuis un autre dossier. Il est bien entendu possible d'utiliser d'autres systèmes de stockage : en base de données, dans un coffre-fort numérique, en génération dynamique - le choix d'un VirtualPathProvider étant alors quelque peu discutable -, les possibilités ne sont peut-être pas infinies mais devraient correspondre à la plupart des besoins.

  • BuildProviders

    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: ,