Configuration en base de données - Partie 2

Dans le post précédent, on avait posé les bases pour pouvoir accéder à notre configuration (à savoir une section dans le web.config qui nous indique quelle ConnectionString utiliser ainsi que la table et sa proc stock pour lire les données).

Désormais il ne nous reste "plus qu'à" écrire quelques classes nous permettant d'y accéder facilement sans trop se poser de questions.

La première est la classe d'accès aux données qui va se contenter d'appeler la procédure stockée et nous renvoyer la valeur de la configuration trouvée.

   1: internal class ConfigurationAccess
   2: {
   3:     #region Private Members
   4:  
   5:     private string _connectionString;
   6:  
   7:     #endregion
   8:  
   9:     #region Constructors
  10:  
  11:     public ConfigurationAccess()
  12:     {
  13:     }
  14:  
  15:     #endregion
  16:  
  17:     #region Public Methods
  18:  
  19:     /// <summary>
  20:     /// Initializes the specified connection string.
  21:     /// </summary>
  22:     /// <param name="connectionString">The connection string.</param>
  23:     public void Initialize(string connectionString)
  24:     {
  25:         _connectionString = connectionString;
  26:     }
  27:  
  28:     /// <summary>
  29:     /// Gets the configuration attribute for a specific application's name and key.
  30:     /// </summary>
  31:     /// <param name="applicationName">Name of the application.</param>
  32:     /// <param name="key">The key.</param>
  33:     /// <returns>The value or <code>null</code> if there's no configuration value</returns>
  34:     public string GetConfigurationAttribute(string applicationName, string key)
  35:     {
  36:         using (SqlConnection connection = ConnectionHelper.CreateOpenedConnection(_connectionString))
  37:         {
  38:             using (SqlCommand command = CommandHelper.CreateCommand("Conf.P_GetConfigurationAttribute", connection))
  39:             {
  40:                 CommandHelper.AddParameter(command, "ApplicationName", SqlDbType.NVarChar, applicationName);
  41:                 CommandHelper.AddParameter(command, "Key", SqlDbType.NVarChar, key);
  42:  
  43:                 return command.ExecuteScalar() as string;
  44:             }
  45:         }
  46:     }
  47:  
  48:     #endregion
  49: }

Rien de bien innovant la dedans ; une méthode d'initialisation afin de pouvoir spécifier sur quelle base de données faire les requêtes, et une seconde qui requête la base de données. Les paramètres fournis lors de l'appel à la procédure stockée sont comme prévus le nom de l'application qui exécute la procédure stockée et la clé de configuration pour laquelle on souhaite obtenir la valeur.

On peut apercevoir l'utilisation de ConnectionHelper.CreateOpenedConnection et CommandHelper.CreateCommand qui sont deux pauvres méthodes de ma création qui ouvrent une connexion et la seconde qui instancie une SqlCommand de type StoredProcedure.

 

Au dessus de cette classe toute bête, nous allons y rajouter une classe ConfigurationController qui va s'occuper principalement de gérer la montée en cache des clés de configuration. En voici une première description :

   1: public class ConfigurationController
   2: {
   3:     #region Private Members
   4:  
   5:     private Cache<string, string> _cache;
   6:     private ConfigurationAccess _confAccess;
   7:  
   8:     private TimeSpan _duration;
   9:  
  10:     private bool _useCache;
  11:  
  12:     #endregion
  13:  
  14:     #region Constructors
  15:  
  16:     public ConfigurationController()
  17:     {
  18:         _confAccess = new JALUM.Utility.Configuration.Database.ConfigurationAccess();
  19:         _confAccess.Initialize(StartupConfiguration.ConnectionString);
  20:         _confAccess = access;
  21:  
  22:         _cache = CacheManager.GetCache<string, string>("Configuration");
  23:         _cache.DefaultSlidingExpiration = false;
  24:     }
  25:  
  26:     #endregion
  27: }

Dans le constructeur, on instancie notre classe d'accès aux données et on l'initialise avec la ConnectionString indiquée dans le web.config (Attention, ici il s'agit de la classe StartupConfiguration qui est une classe statique permettant d'accéder plus simplement aux informations renvoyées par StartupConfigurationSection).

On peut aussi y voir le cache qui contiendra le couple clé/valeur ainsi qu'un TimeSpan qui contient la durée de cache et un bool qui déterminera si on doit utiliser le cache ou non.

Mais comment le contrôleur de configuration sait si il doit monter les données en cache, et si oui pour combien de temps ? En allant les lire en configuration bien sûr. Pour cela on va exposer deux propriétés UseCache et CacheDuration. Sans plus attendre en voici le code :

   1: public bool UseCache
   2: {
   3:     get
   4:     {
   5:         string key = "Configuration.UseCache";
   6:         string returnValue;
   7:         if (_cache.ContainsKey(key))
   8:         {
   9:             return _useCache;
  10:         }
  11:  
  12:         returnValue = _confAccess.GetConfigurationAttribute(StartupConfiguration.ApplicationName, key);
  13:         if (string.IsNullOrEmpty(returnValue))
  14:         {
  15:             returnValue = _confAccess.GetConfigurationAttribute(null, key);
  16:         }
  17:  
  18:         returnValue = returnValue ?? "true";
  19:         _useCache = bool.Parse(returnValue);
  20:         _cache.Add(key, returnValue, CacheDuration);
  21:  
  22:         return _useCache;
  23:     }
  24: }
  25:  
  26: public TimeSpan CacheDuration
  27: {
  28:     get
  29:     {
  30:         string key = "Configuration.CacheDuration";
  31:         string returnValue;
  32:         if (_cache.ContainsKey(key))
  33:         {
  34:             return _duration;
  35:         }
  36:  
  37:         returnValue = _confAccess.GetConfigurationAttribute(StartupConfiguration.ApplicationName, key);
  38:         if (string.IsNullOrEmpty(returnValue))
  39:         {
  40:             returnValue = _confAccess.GetConfigurationAttribute(null, key);
  41:         }
  42:  
  43:         returnValue = returnValue ?? "00:10:00";
  44:  
  45:         _duration = TimeSpan.Parse(returnValue);
  46:         _cache.Add(key, returnValue, _duration);
  47:  
  48:         return _duration;
  49:     }
  50: }

En lisant le code on peut rapidement se poser deux questions :

  • Pourquoi deux appels par propriété à la classe d'accès aux données ?
  • Pourquoi avoir des variables de classe (_useCache et _duration) alors que ces deux données sont forcement dans le cache ?

A la première question, je te répondrai simplement parce que on effectue d'abord une première requête afin de consulter la clé pour l'application en cours, et une seconde pour la configuration commune à toutes les applications si la première requête n'a pas renvoyé de résultats.

Et à la seconde, il s'agit purement et simplement d'augmenter les performances. Si ces deux variables n'existaient pas, à chaque appel à ces propriétés, le code serait obliger de faire un Parse de la string pour obtenir l'objet souhaité (un bool ou un TimeSpan). Alors qu'en vérifiant seulement si la clé et présente et en renvoyant le dernier résultat connu, on évite ce Parse fastidieux et couteux.

Il ne reste plus que l'obtention d'une valeur de configuration générale à définir et cette classe sera terminée. Rien de bien particulier à dire si l'on a déjà lu le code ci-dessus :

   1: public string GetConfigurationValue(string key)
   2: {
   3:     bool _useCache = UseCache;
   4:  
   5:     string returnValue;
   6:     if (_useCache && _cache.TryGetValue(key, out returnValue))
   7:     {
   8:         return returnValue;
   9:     }
  10:  
  11:     returnValue = _confAccess.GetConfigurationAttribute(StartupConfiguration.ApplicationName, key);
  12:     if (string.IsNullOrEmpty(returnValue))
  13:     {
  14:         returnValue = _confAccess.GetConfigurationAttribute(null, key);
  15:     }
  16:  
  17:     if (_useCache)
  18:     {
  19:         _cache.Add(key, returnValue, CacheDuration);
  20:     }
  21:  
  22:     return returnValue;
  23: }

 

Tout cela c'est bien beau, mais ca permet pas d'organiser facilement sa configuration, ni de la hiérarchiser. Cela je l'exposerai dans un prochain post.

Sur ce, je vais célébrer mon anniversaire qui tombe pile poil aujourd'hui, alors soyez indulgent sur les commentaires si il y en a :-) .

Configuration en base de données - Partie 1

Je reviens après de longs jours d'absence, pour cause de fin de projet et accessoirement de vacances, que je reviens pour écrire ce post.

Comme je l'ai indiqué, les posts que j'écris sont en rapport direct avec un projet que j'essaye de mener depuis de nombreux mois. Ils n'entendent pas révolutionner la façon dont les choses sont faîtes, ni utiliser les dernières technologies à la mode sauf si ces dernières peuvent apporter un réel bénéfice au projet.

Ainsi après avoir établi le Cache, élément indispensable (selon moi), il vient une autre partie que je trouve tout aussi indispensable, une gestion de la configuration. Le Framework propose pour cela une gestion de la configuration à l'aide des sections dans les fichiers .config . Cette configuration fonctionne très bien et est qui plus est très pratique.

Cependant lorsque l'on travaille dans une ferme, modifier ces fichiers de configuration impose de redéployer sur l'ensemble de la ferme (ce qui implique potentiellement d'autres services tel que celui chargé de la production par exemple), et fait aussi redémarrer le site web sur chaque frontal. De même, lorsque une configuration est partagée entre plusieurs applications, impacter toutes les applications lorsqu'une modification a lieu n'est pas forcement une chose aisée non plus.

Pour palier ces désagréments, j'ai eu l'occasion de mettre en place chez mon client adoré un petit système simple de configuration géré en base de données dont je me propose d'expliquer ici son fonctionnement.

 

Comme indiqué quelques lignes plus haut, cette configuration doit pouvoir être commune à plusieurs applications. Cependant chaque application doit avoir la possibilité de surcharger cette valeur par ses valeurs propres. Enfin Il faut que l'information en base de données soit un minimum structurée et que les valeurs chargées depuis la base de données soient montées en cache.

Tout d'abord, définissons le moyen d'identifier l'application en cours, et le moyen d'accéder à la base de donnée. Il faut bien un minimum de configuration dans des fichiers, cela se définira donc au niveau du .config grâce à  une section créée pour l'occasion. Voici le code bien compliqué de la section :

   1: internal class StartupConfigurationSection : ConfigurationSection
   2: {
   3:     [ConfigurationProperty("ApplicationName", IsRequired=true)]
   4:     public string ApplicationName
   5:     {
   6:         get { return (string)this["ApplicationName"]; }
   7:     }
   8:  
   9:     [ConfigurationProperty("ConnectionStringName", IsRequired = false, DefaultValue = "Default")]
  10:     public string ConnectionStringName
  11:     {
  12:         get { return (string)this["ConnectionStringName"]; }
  13:     }
  14: }

que l'on configure comme ceci dans le .config par exemple :

   1: <configSections>
   2:     <section name="startupConfiguration" type="JALUM.Utility.Configuration.StartupConfigurationSection, JALUM.Utility.Configuration"/>
   3: </configSections>
   4:  
   5: <startupConfiguration ApplicationName="ConfigurationManager"/>
   6:  
   7: <connectionStrings>
   8:     <add name="Default" connectionString="Data Source=localhost;Initial Catalog=Configuration;Integrated Security=SSPI"/>
   9: </connectionStrings>

Ensuite en base, il est nécessaire d'avoir une petite table qui va contenir les données sous la forme clé / valeur (les tailles des champs sont donnés à titre indicatif) :

Table Définition

et la procédure stockée qui va bien avec pour récupérer les valeurs :

   1: CREATE PROCEDURE [Conf].[P_GetConfigurationAttribute]
   2:     -- Add the parameters for the stored procedure here
   3:     @ApplicationName nvarchar(64) = null,
   4:     @Key nvarchar(128)
   5: AS
   6: BEGIN
   7:  
   8:     IF (@ApplicationName IS NULL)
   9:     BEGIN
  10:         SELECT [Value]
  11:         FROM [T_ConfigurationAttributes] (NOLOCK)
  12:         WHERE [ApplicationName] IS NULL AND [Key] = @Key
  13:     END
  14:     ELSE
  15:     BEGIN
  16:         SELECT [Value]
  17:         FROM [T_ConfigurationAttributes] (NOLOCK)
  18:         WHERE [ApplicationName] = @ApplicationName AND [Key] = @Key
  19:     END
  20: END

 

Et voilà pour la première partie de la future couche de configuration. Ok rien de bien fabuleux non plus mais petit à petit l'oiseau fait son nid...

Singleton générique

Tout d'abord, c'est quoi un Singleton ? Pour résumer très vite (Google est ton ami si tu veux plus de détails), un singleton est une instance unique d'une classe qui sera utilisée tout au long d'un processus. Le but de cette démarche est surtout pour moi de gagner en rapidité lors de l'écriture et aussi lors de l'exécution.

Imaginons que j'ai une classe MyClass et à l'intérieur une méthode DoThings. Si je veux appeler cette méthode je suis obligé de faire :

   1: MyClass myClass = new MyClass(); 
   2: myClass.DoThings();

et cela chaque fois que je voudrai appeler la méthode. C'est une perte de temps dans l'écriture du code lui-même mais aussi une perte de temps dans l'exécution puisqu'à chaque fois il devra instancier une nouvelle fois la classe (et tout ce que cela implique niveau mémoire etc). Dans ce cas-là, on pourrait se dire qu'il suffirait de mettre DoThings en méthode statique. De manière générale je suis un peu contre mettre les choses en static sauf si l'utilité est réellement prouvée. Dans notre cas, c'est seulement parce que l'on est feignant, donc je m'y refuse.

La méthode la plus simple, et la plus répandue aussi je crois, pour obtenir le singleton serait de définir des méthodes statiques dans la classe pour effectuer cela.

   1: public class MyClass
   2: {
   3:     private static MyClass _singleton;
   4:  
   5:     static MyClass()
   6:     {
   7:         _singleton = new MyClass();
   8:     }
   9:  
  10:     public static MyClass GetInstance()
  11:     {
  12:         return _singleton;
  13:     }
  14:  
  15:     public void DoThings()
  16:     {
  17:     }
  18: }

Le constructeur statique instancie une fois la classe et la stocke dans une variable statique elle aussi. A chaque appel de la méthode statique GetInstance, on retourne toujours la même instance. Ainsi là où l'on avait les deux lignes définies plus haut, on a plus que ceci :

   1: MyClass.GetInstance().DoThings();

Il me semble que c'est déjà pas mal non ? Et en plus ca marche très bien. Le seul petit problème, c'est que l'on doit se taper la variable, le constructeur et la méthode dans chaque classe où l'on souhaite avoir le singleton. La solution ? Les génériques !

Grâce aux génériques on va pouvoir se créer des singletons une bonne fois pour toutes, et à un seul endroit en plus. Sans plus attendre, le code :

   1: public static class Singleton<T> where T : class, new()
   2: {
   3:     private static T _singleton;
   4:  
   5:     static Singleton()
   6:     {
   7:         _singleton = new T();
   8:     }