J'ai créé ce blog dans un seul but...

Bonjour,

j'ai créé ce blog dans un seul but : apporter à ceux qui en le veulent la maigre expérience que j'ai accumulé durant mes 3 années de dur labeur en tant que développeur, et ce, principalement en diffusant du code que j'ai écrit afin de composer un petit framework nommé JALUM venant se rajouter au Framework.net déjà bien complet.

Cependant je n'ai pas eu le temps ni la motivation que cela nécessitait. Pour compenser cette faiblesse de ma part, et offrir tout de même la possibilité à ceux qui le souhaite de consulter mon travail, j'ai publié sur CodePlex l'ensemble du code que j'ai écrit jusqu'à présent. Il est désormais disponible sur http://www.codeplex.com/jalum.

Ce dernier a été écrit dans un but de grande performance et haute disponibilité, chose à laquelle je suis pense être parvenu car il est aujourd'hui utilisé sur un site internet accueillant chaque jour plus de 300 000 utilisateurs. Par conséquent, son fonctionnement reste très simple sur le principe, mais est il me semble assez souple pour convenir à un grand nombre de problématique. Ceux qui auront eu le courage de lire ce post en entier et d'aller le consulter sur Codeplex pourront disposer d'un certain nombre de fonctionnalités déjà exposées sur mon blog ainsi que d'autres dont j'aurai souhaité faire état dans ce blog.

En voici un résumé:

  • Système de cache
  • Gestion de configuration
  • Accès aux données et mapping DataReader -> Objet
  • Logging
  • Provider de globalisation pour un site asp.net
  • Cryptographie
  • Stockage et traitement d'images en base de données
  • Quelques outils pour site web dont une gestion sécurisée des urls et cookies

 

J'espère que cela pourra être utile à certains...

 

 

[Tips] Eviter le double click sur un postback

Aujourd'hui je me suis heurté à un petit problème ô combien classique :
Comment interdire les double click sur un Button / LinkButton sans rendre le bouton disabled en grisé tout moche ?

J'ai regarder les solutions sur notre grand ami google, et la plupart utilisent disabled ce que l'on souhaite éviter pour des raisons esthétiques.
La solution que j'ai trouvé cet après-midi (mais qui est loin d'être parfaite pour le moment) : positionner un attribut en plus sur le control pour savoir si on interdit ou non le click, et positionner un appel JavaScript sur le onclick du bouton.
Le JavaScript s'occupant de gérer le blocage du click devant s'assurer au préalable que la validation est passée avec succès avant de bloquer le click.

Voici une première ébauche du code rédigé aujourd'hui :

   1: public class DoubleClickExtender : Control
   2: {
   3:     public DoubleClickExtender()
   4:     {
   5:         PreRender += new EventHandler(DoubleClickExtender_PreRender);
   6:     }
   7:  
   8:     void DoubleClickExtender_PreRender(object sender, EventArgs e)
   9:     {
  10:         // Register validation script
  11:         Page.ClientScript.RegisterClientScriptResource(GetType(), "Vp.Core.Web.UI.Extenders.js");
  12:  
  13:         WebControl control = NamingContainer.FindControl(TargetControlID) as WebControl;
  14:         IButtonControl button = control as IButtonControl;
  15:  
  16:         if (control != null && button != null)
  17:         {
  18:             control.Attributes.Add("onclick", "return CheckAndDisableControl(this);");
  19:             RegisterExpandoAttribute(control, "validationgroup", button.ValidationGroup, true);
  20:             RegisterExpandoAttribute(control, "isblocked", "false", true);
  21:         }
  22:     }
  23:  
  24:     [IDReferenceProperty]
  25:     public string TargetControlID
  26:     {
  27:         get { return ViewState["TargetControlID"] as string; }
  28:         set { ViewState["TargetControlID"] = value; }
  29:     }
  30: }

Et le JavaScript qui va avec :

   1: function CheckAndDisableControl(button)
   2: {
   3:     if (typeof(Page_Validators) == "undefined")
   4:     {
   5:         return Block(button);
   6:     }
   7:     
   8:     if (typeof(Page_ClientValidate) == 'function')
   9:     {
  10:         Page_ClientValidate(button.validationgroup);
  11:         if(Page_IsValid)
  12:         {
  13:             return Block(button)
  14:         }
  15:     }
  16:     else
  17:     {
  18:         return Block(button);
  19:     }
  20: }
  21:  
  22: function Block(control)
  23: {
  24:     if (control.isblocked == "false")
  25:     {
  26:         control.isblocked = "true";
  27:         return true;
  28:     }
  29:     else
  30:     {
  31:         return false;
  32:     }
  33: }

 

Voilà, c'est pas encore parfait, mais ça peut donner des idées à certains qui auraient le même problème ... :-)

Mapping Attribute IV - Génération d'IL :-)

Pour conclure cette série d'article sur le mapping, on va s'attaquer à un mapping par IL généré. On va donc créer une nouvelle classe ILMapper<E> qui va dériver de BaseMapper<E> définit dans un article précédent.

Tout d'abord on va définir un delegate qui nous servira de point d'entrée lorsque l'on voudra effectuer notre mapping :

   1: private delegate long MapDelegate(E entity, SqlDataReader reader);
   2:  
   3: private MapDelegate _internalILMap = null;

Ensuite nous allons surcharger la méthode InitializeMapping() du BaseMapper<E> afin de générer notre IL. Cela passe par la création d'une DynamicMethod que l'on rattachera à E, le type que l'on souhaite mapper. Et pour conclure, affecte cette DynamicMethod à notre delegate créé précédement.

   1: public override void InitializeMapping()
   2: {
   3:     // Extract all the mapping informations
   4:     base.InitializeMapping();
   5:  
   6:     // Create the dynamic method which will make the mapping
   7:     DynamicMethod method = new DynamicMethod("InternalILMap", typeof(long), new Type[] { typeof(E), typeof(SqlDataReader) }, typeof(E), true);
   8:     ILGenerator ILOut = method.GetILGenerator();
   9:     GenerateMainMethod(ILOut);
  10:     _internalILMap = (MapDelegate)method.CreateDelegate(typeof(MapDelegate));
  11: }

Mais passons un peu aux choses sérieuses avec la génération d'IL. Encore une fois je vous renvoie vers l'article de Michel Perfetti dont je me suis inspiré. Son article détaille un peu plus le fonctionnement de la génération d'IL.

Le but du code qui suit est d'itérer sur l'ensemble des colonnes présentes dans le datareader et y récupérer le nom de la colonne. Le code en C# ressemble à ceci :

   1: public long Map(News news, SqlDataReader reader)
   2: {
   3:     long bitmask = 0;
   4:     int index = 0;
   5:     int columnCount = reader.FieldCount;
   6:     while (index < columnCount)
   7:     {
   8:         object readerValue;
   9:         object convertedValue;
  10:         string columnName = reader.GetName(index);
  11:  
  12:         // Mapping individual fields
  13:  
  14:         index++;
  15:     }
  16:     return bitmask;
  17: }

Et voici le code qui permet de générer ce simple while :

   1: private void GenerateMainMethod(ILGenerator ILOut)
   2: {
   3:     MethodInfo readerFieldCount = typeof(SqlDataReader).GetProperty("FieldCount").GetGetMethod();
   4:     MethodInfo readerGetName = typeof(SqlDataReader).GetMethod("GetName");
   5:  
   6:     // Define label for loop through the reader columns
   7:     Label LoopSartLabel = ILOut.DefineLabel();
   8:     Label LoopEndLabel = ILOut.DefineLabel();
   9:     Label EndGlobalIfLabel = ILOut.DefineLabel();
  10:  
  11:     #region Locals Declaration
  12:  
  13:     // Declare local variables
  14:     LocalBuilder bitMask = ILOut.DeclareLocal(typeof(long));
  15:     LocalBuilder index = ILOut.DeclareLocal(typeof(int));
  16:     LocalBuilder columName = ILOut.DeclareLocal(typeof(string));
  17:     LocalBuilder columnCount = ILOut.DeclareLocal(typeof(int));
  18:     LocalBuilder readerValue = ILOut.DeclareLocal(typeof(object));
  19:     LocalBuilder convertedValue = ILOut.DeclareLocal(typeof(object));
  20:  
  21:     #endregion
  22:  
  23:     // Gets the number of column
  24:     ILOut.Emit(OpCodes.Ldarg_1);
  25:     ILOut.Emit(OpCodes.Callvirt, readerFieldCount);
  26:     ILOut.Emit(OpCodes.Stloc_3);
  27:  
  28:  
  29:     // Loop Start - ie Start of the while
  30:     ILOut.MarkLabel(LoopSartLabel);
  31:  
  32:  
  33:     // Check if there still a column to check
  34:     ILOut.Emit(OpCodes.Ldloc_1);
  35:     ILOut.Emit(OpCodes.Ldloc_3);
  36:     ILOut.Emit(OpCodes.Clt);
  37:     ILOut.Emit(OpCodes.Brfalse, LoopEndLabel);
  38:  
  39:     // Retrieve the columName
  40:     ILOut.Emit(OpCodes.Ldarg_1);
  41:     ILOut.Emit(OpCodes.Ldloc_1);
  42:     ILOut.Emit(OpCodes.Callvirt, readerGetName);
  43:     ILOut.Emit(OpCodes.Stloc_2);
  44:  
  45:     foreach (KeyValuePair<string, FieldMapped> mapping in _specificMapping)
  46:     {
  47:         GenerateFieldMapping(ILOut, mapping.Value, EndGlobalIfLabel);
  48:     }
  49:  
  50:  
  51:     ILOut.MarkLabel(EndGlobalIfLabel);
  52:  
  53:     // Increment the index
  54:     ILOut.Emit(OpCodes.Ldloc_1);
  55:     ILOut.Emit(OpCodes.Ldc_I4_1);
  56:     ILOut.Emit(OpCodes.Add);
  57:     ILOut.Emit(OpCodes.Stloc_1);
  58:  
  59:     // Return to the begin of the while
  60:     ILOut.Emit(OpCodes.Br, LoopSartLabel);
  61:  
  62:     // End of the while
  63:     ILOut.MarkLabel(LoopEndLabel);
  64:  
  65:     ILOut.Emit(OpCodes.Ldloc_0);
  66:     ILOut.Emit(OpCodes.Ret);
  67: }

 

Maintenant, nous allons nous attarder le code qui va s'occuper de faire l'affectation de la valeur contenue dans le DataReader dans la variable ou la propriété de l'objet. On va distinguer deux cas : lorsque le type accepte les valeurs nulles, ou bien lorsque le type ne l'accepte pas. Dans le dernier si le DataReader fournit une valeur DbNull, alors une exception est levée. Voici les deux codes C# que l'on peut obtenir :

   1: if (string.Equals(columnName, "NewsId", StringComparison.InvariantCultureIgnoreCase))
   2: {
   3:     columnValue = reader[index];
   4:     if (columnValue is DBNull)
   5:     {
   6:         throw new ApplicationException("Field mapped on 'NewsId' cannot be null");
   7:     }
   8:     convertedValue = MappingHelper.ConvertTo(typeof(int), columnValue);
   9:     news._id = (int)convertedValue;
  10:     bitmask |= 1;
  11: }

ou bien :

   1: if (string.Equals(columnName, "Name", StringComparison.InvariantCultureIgnoreCase))
   2: {
   3:     columnValue = reader[index];
   4:     if (columnValue is DBNull)
   5:     {
   6:         convertedValue = null;
   7:     }
   8:     else
   9:     {
  10:         convertedValue = MappingHelper.ConvertTo(typeof(string), columnValue);
  11:     }
  12:     news._name = (string)convertedValue;
  13:     num |= 2;
  14: }

 

Et voici le code qui permet de générer ces affectations (attention le code est assez long) :

   1: private void GenerateFieldMapping(ILGenerator ILOut, FieldMapped mapping, Label EndGlobalIfLabel)
   2: {
   3:     MethodInfo readerGetItem = typeof(SqlDataReader).GetMethod("get_Item", new Type[] { typeof(int) });
   4:     MethodInfo stringEquals = typeof(string).GetMethod("Equals", new Type[] { typeof(string), typeof(string), typeof(StringComparison) });
   5:     MethodInfo convertTo = typeof(MappingHelper).GetMethod("ConvertTo");
   6:     MethodInfo getType = typeof(Type).GetMethod("GetTypeFromHandle");
   7:     ConstructorInfo exConst = typeof(ApplicationException).GetConstructor(new Type[] { typeof(string) });
   8:  
   9:     Label EndLocalIfLabel = ILOut.DefineLabel();
  10:     Label CallToConvert = ILOut.DefineLabel();
  11:     Label MakeAffectation = ILOut.DefineLabel();
  12:  
  13:     Type destType = mapping.Field != null ? mapping.Field.FieldType : mapping.Property.PropertyType;
  14:  
  15:     // Check if the current mapping field column name is equal to the current reader's column name
  16:     ILOut.Emit(OpCodes.Ldloc_2);
  17:     ILOut.Emit(OpCodes.Ldstr, mapping.MappingAttribute.ColumnName);
  18:     ILOut.Emit(OpCodes.Ldc_I4_3);
  19:     ILOut.Emit(OpCodes.Call, stringEquals);
  20:     ILOut.Emit(OpCodes.Brfalse, EndLocalIfLabel);
  21:  
  22:     // Get the item from the data reader for the current column
  23:     ILOut.Emit(OpCodes.Ldarg_1);
  24:     ILOut.Emit(OpCodes.Ldloc_1);
  25:     ILOut.Emit(OpCodes.Callvirt, readerGetItem);
  26:     ILOut.Emit(OpCodes.Stloc, 4);
  27:  
  28:     // Check if the current value is null (DBNull)
  29:     ILOut.Emit(OpCodes.Ldloc, 4);
  30:     ILOut.Emit(OpCodes.Isinst, typeof(DBNull));
  31:     ILOut.Emit(OpCodes.Ldnull);
  32:     ILOut.Emit(OpCodes.Cgt_Un);
  33:     ILOut.Emit(OpCodes.Ldc_I4_0);
  34:     ILOut.Emit(OpCodes.Ceq);
  35:     ILOut.Emit(OpCodes.Brtrue, CallToConvert);
  36:  
  37:     // Init to null the converted value if possible
  38:     if (destType.IsValueType && !destType.IsGenericType)
  39:     {
  40:         ILOut.Emit(OpCodes.Ldstr, "Field mapped on '" + mapping.MappingAttribute.ColumnName + "' cannot be null");
  41:         ILOut.Emit(OpCodes.Newobj, exConst);
  42:         ILOut.Emit(OpCodes.Throw);
  43:     }
  44:     else
  45:     {
  46:         ILOut.Emit(OpCodes.Ldnull);
  47:         ILOut.Emit(OpCodes.Stloc, 5);
  48:     }
  49:  
  50:     ILOut.Emit(OpCodes.Br, MakeAffectation);
  51:  
  52:     // Call the ConvertTo Method
  53:     ILOut.MarkLabel(CallToConvert);
  54:     if (destType.IsValueType && destType.IsGenericType)
  55:     {
  56:         ILOut.Emit(OpCodes.Ldtoken, destType.GetGenericArguments()[0]);
  57:     }
  58:     else
  59:     {
  60:         ILOut.Emit(OpCodes.Ldtoken, destType);
  61:     }
  62:     ILOut.Emit(OpCodes.Call, getType);
  63:     ILOut.Emit(OpCodes.Ldloc, 4);
  64:     ILOut.Emit(OpCodes.Call, convertTo);
  65:     ILOut.Emit(OpCodes.Stloc, 5);
  66:     ILOut.Emit(OpCodes.Br, MakeAffectation);
  67:  
  68:  
  69:     // Do the affectation of the computed value
  70:     ILOut.MarkLabel(MakeAffectation);
  71:  
  72:     ILOut.Emit(OpCodes.Ldarg_0);
  73:     ILOut.Emit(OpCodes.Ldloc, 5);
  74:  
  75:     if (destType.IsValueType)
  76:     {
  77:         ILOut.Emit(OpCodes.Unbox_Any, destType);
  78:     }
  79:     else
  80:     {
  81:         ILOut.Emit(OpCodes.Castclass, destType);
  82:     }
  83:  
  84:     if (mapping.Field != null)
  85:     {
  86:         ILOut.Emit(OpCodes.Stfld, mapping.Field);
  87:     }
  88:     else
  89:     {
  90:         ILOut.Emit(OpCodes.Callvirt, mapping.Property.GetSetMethod());
  91:     }
  92:  
  93:     ILOut.Emit(OpCodes.Ldloc_0);
  94:     ILOut.Emit(OpCodes.Ldc_I8, mapping.MappingAttribute.BitFieldIndex);
  95:     ILOut.Emit(OpCodes.Or);
  96:     ILOut.Emit(OpCodes.Stloc_0);
  97:  
  98:  
  99:     // Close the If-Else code
 100:     ILOut.Emit(OpCodes.Br, EndGlobalIfLabel);
 101:     ILOut.MarkLabel(EndLocalIfLabel);
 102: }

J'ai essayé de mettre quelques commentaires pour rendre le code un peu plus lisible et j'espère qu'avec le résultat en C# juste au dessus le code sera suffisement compréhensible.

J'en profite pour poser une question, comment savoir si un type peut recevoir une valeur nulle ? Le seul moyen que j'ai trouvé est dans le code, il s'agit de tester destType.IsValueType && !destType.IsGenericType . Si quelqu'un connait un meilleur moyen ....

 

Et voilà ce dernier article qui conclue sur le moyen que j'utilise pour effectuer le mapping entre mes objets et mon DataReader.

FindControl... qui ne trouve pas trop

Aujourd'hui j'ai été confronté à un un petit problème. En voici un résumé :

J'ai une MasterPage qui présente deux ContentPlaceHolder : SideContent et MainContent. Dans le SideContent se trouvent tous mes contrôles qui vont me servir à effectuer des filtres dans une recherche. Dans le MainContent, j'ai un GridView ainsi qu'un ObjectDataSource qui vont servir à afficher les résultats après application de mes filtres. Afin d'informer mon ObjectDataSource que les résultats doivent tenir compte de la valeur de certains contrôles, j'y rajoute des ControlParameter.

Jusque là rien de bien particulier dans l'utilisation de l'ObjectDataSource, je l'ai déjà fait plusieurs fois sans aucun problème. Mais ce coup-ci j'obtiens une erreur m'indiquant qu'il ne trouve pas mes contrôles dans la page. Avec un petit coup de Reflector je comprends mieux pourquoi il ne trouve pas mon contrôle mais reste un peu sur ma faim jusqu'à ce que je tombe sur un article de Cyril traitant justement de la méthode FindControl utilisée par le ControlParameter.

Et là je suis sauvé, j'y ai découvert (je n'y avais jamais été confronté jusque là) que l'on pouvait nommer les ID des contrôles en y incorporant les parents. Ainsi au lien de seulement spécifier ControlID="txtMessage" on peut préciser ControlID="SideContent:txtMessage".

 

Alors c'est pas grand chose, mais je suis content d'avoir appris ça aujourd'hui, et j'espère que ça servira aussi à des potentielles âmes perdues sur le net.

Mapping Attribute III

Je fait suite aux articles précédants avec ce nouvel opus qui concernent le mapping à proprement parlé. Afin d'expliquer plus clairement le moyen d'y parvenir, il est plus prudent de commencer par une méthode relativement simple : par réflection.

Cela se résume à une méthode et une seule que voici :

   1: public override long Map(E entity, SqlDataReader reader)
   2: {
   3:     long bitMap = 0;
   4:  
   5:     for (int i = 0; i < reader.FieldCount; i++)
   6:     {
   7:         string columName = reader.GetName(i);
   8:         FieldMapped fieldMapped = null;
   9:  
  10:         if (_specificMapping.TryGetValue(columName, out fieldMapped))
  11:         {
  12:             if (fieldMapped.Field != null)
  13:             {
  14:                 if (reader[ i ] is DBNull)
  15:                 {
  16:                     fieldMapped.Field.SetValue(entity, null);
  17:                 }
  18:                 else
  19:                 {
  20:                     fieldMapped.Field.SetValue(entity, MappingHelper.ConvertTo(fieldMapped.Field.FieldType, reader[ i ]));
  21:                 }
  22:             }
  23:             else
  24:             {
  25:                 if (reader[ i ] is DBNull)
  26:                 {
  27:                     fieldMapped.Property.SetValue(entity, null, null);
  28:                 }
  29:                 else
  30:                 {
  31:                     fieldMapped.Property.SetValue(entity, MappingHelper.ConvertTo(fieldMapped.Property.PropertyType, reader[ i ]), null);
  32:                 }
  33:             }
  34:             bitMap |= fieldMapped.MappingAttribute.BitFieldIndex;
  35:         }
  36:     }
  37:  
  38:     return bitMap;
  39: }

 

Qu'est-ce que l'on fait ici ? Pour chacune des colonnes du DataReader, on vérifie si le nom de la colonne est présent dans les mapping que l'on découvert grâce au code précédant. Si c'est le cas, on affecte la valeur trouvée au Field ou Property. La valeur est convertie grâce à une petite classe que je ne détaillerai pas ici mais qui se contente de faire des ConvertTo ou des cast.

Dernière petite chose, le bitmap est mis à jour en fonction des champs que l'on aura réussi à mapper :-)

 

Bon, c'était easy comme tout, prochain article on fait la même chose mais avec de l'IL généré dynamiquement.

MappingAttribute - Part II

Maintenant qu'il est possible de tagger les champs ou propriétées de notre classe, nous allons nous attaquer au code qui va nous permettre de lire ces informations afin de préparer le mapping.

Pour cela nous allons faire une première classe FieldMapped qui va définir chacun des mapping :

   1: internal class FieldMapped
   2: {
   3:     public MappingAttribute MappingAttribute;
   4:     public FieldInfo Field;
   5:     public PropertyInfo Property;
   6:  
   7:     public FieldMapped(MappingAttribute mappingAttribute, FieldInfo field, PropertyInfo property)
   8:     {
   9:         MappingAttribute = mappingAttribute;
  10:         Field = field;
  11:         Property = property;
  12:     }
  13: }

On retrouve ici le MappingAttribute qui aura été ajouté à la classe ainsi que le Field ou la Property sur lequel le MappingAttribute est appliqué. Voyons maintenant comment remplir cette classe. Ce sera le rôle de la classe BaseMapper<E> où E sera la classe qui devra être mappée. Sans plus attendre voilà le code :

   1: public abstract class BaseMapper<E>
   2: {
   3:     #region Protected Members
   4:  
   5:     protected Dictionary<string, FieldMapped> _specificMapping;
   6:  
   7:     #endregion
   8:  
   9:     #region Public Methods
  10:  
  11:     public virtual void InitializeMapping()
  12:     {
  13:         if (_specificMapping == null)
  14:         {
  15:             _specificMapping = new Dictionary<string, FieldMapped>();
  16:  
  17:             List<FieldInfo> fields = new List<FieldInfo>();
  18:             List<PropertyInfo> properties = new List<PropertyInfo>();
  19:  
  20:             Type type = typeof(E);
  21:  
  22:             while (type != typeof(object))
  23:             {
  24:                 fields.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Instance));
  25:                 properties.AddRange(type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.Instance));
  26:                 type = type.BaseType;
  27:             }
  28:  
  29:  
  30:             for (int j = 0; j < fields.Count; j++)
  31:             {
  32:                 MappingAttribute[] attributes = (MappingAttribute[])fields[j].GetCustomAttributes(typeof(MappingAttribute), true);
  33:                 if (attributes != null && attributes.Length > 0)
  34:                 {
  35:                     if (_specificMapping.ContainsKey(attributes[0].ColumnName))
  36:                     {
  37:                         throw new ApplicationException("The column name is already mapped");
  38:                     }
  39:                     _specificMapping.Add(attributes[0].ColumnName, new FieldMapped(attributes[0], fields[j], null));
  40:                 }
  41:             }
  42:  
  43:             for (int j = 0; j < properties.Count; j++)
  44:             {
  45:                 MappingAttribute[] attributes = (MappingAttribute[])properties[j].GetCustomAttributes(typeof(MappingAttribute), true);
  46:                 if (attributes != null && attributes.Length > 0)
  47:                 {
  48:                     if (_specificMapping.ContainsKey(attributes[0].ColumnName))
  49:                     {
  50:                         throw new ApplicationException("The column name is already mapped");
  51:                     }
  52:                     _specificMapping.Add(attributes[0].ColumnName, new FieldMapped(attributes[0], null, properties[j]));
  53:                 }
  54:             }
  55:         }
  56:     }
  57:  
  58:     public abstract ulong Map(E entity, SqlDataReader reader);
  59:  
  60:     #endregion
  61: }

Je pense que le code parle pour lui-même mais grosso modo, qu'est-ce que l'on fait ? Tout d'abord on récupère l'ensemble des Fields et des Properties au sein du type de notre objet ainsi que sur tous les types dont il dérive (ligne 20 à 27). De tous ces fields/properties on recherche tous ceux qui ont attribut du type MappingAttribute et on rajoute une nouvelle instance de FieldMapped dans un dictionnaire indexé sur le nom de la colonne afin d'accroître la vitesse lorsque l'on fera le mapping.

 

Voilà pour cette deuxième partie de l'article. Dans la prochaine partie, on effectuera le mapping à proprement parler à l'aide de Reflection pour comprendre exactement ce que l'on veut obtenir, et on terminera par un dernier article plus complexe où de l'IL sera généré afin d'augmenter les performances.

MappingAttribute

Après 4 mois d'abstinence, je reviens avec un post plutôt simple, voir très simple, mais qui va servir d'introduction à une série plus longue sur du code que j'ai recement écrit pour mon client unique et favoris. J'ai donc eu l'occasion de mettre en place un un mini framework. Pour la petite histoire, mes articles précédents font partie de ce framework, à savoir principalement : les singletons (sujet qui fait grand débat), mon propre cache et la configuration (dont l'explication reste inachevée mais je n'avais pas l'impression que cela captivait énormément les rares personnes qui venaient se perdre sur ce blog).

Mais revenons à nos moutons ; une des fonctions de ce mini framework est de gérer les entités, leurs chargement depuis la base de données, et le mapping entre ces entitées et le DataReader résultant de l'exécution d'une procédure stockée. Dans le service auquel j'appartiens, la version actuelle du code faisait un mapping écrit à la main par les developpeurs :

  1: monObjet.MaPropriété1 = (MonType)dataReader["MaColonne1"];
  2: monObjet.MaPropriété2 = (MonType1)dataReader["MaColonne2"];
  3: monObjet.MaPropriété3 = (MonType2)dataReader["MaColonne3"];
  4: monObjet.MaPropriété4 = dataReader["MaColonne4"] as MonType4;
  5: monObjet.MaPropriété5 = (MonType5)dataReader["MaColonne5"];

Etant un gros feignant, je voulais quelque chose de moins fastidieux, plus sûr, mais qui ne pénalise pas non plus la performance. Qui plus est, j'avais en tête un système permettant de ne loader que partiellement les objets, il me fallait donc que le code précédent prenne aussi en compte la possibilité que la colonne ne soit pas présente dans le DataReader (par exemple je veux une liste de personne pour afficher leur nom et prénom, cela ne sert donc à rien de récupérer leur sexe ou date de naissance). La génération avec des outils tels que CodeSmith aurait certainement pu faire l'affaire mais je n'en suis pas un grand fan, et c'est aussi beaucoup moins marrant que de coder. C'est pourquoi je me suis mis à l'écrire d'un mapper.

Je me suis fortement inspiré du code de Michel Perfetti qui avait fait un peu la même chose. La différence entre son code et le mien, c'est que Michel se basait sur l'index des colonnes pour effectuer son mapping, or cela n'était pas possible dans mon cas, puisque cet ordre n'était pas prédéfini à l'avance, le nombre de colonnes changeant à chaque requête.

 

J'ai donc commencer par le commencement, créer mon propre attribut de mapping qui me servira à faire le lien entre ma variable de classe ou bien une propriétée de ma classe et la colonne de mon DataReader. Cet attribut devait avoir comme propriétés exposées :

  • ColumnName : le nom de la colonne dans le DataReader
  • BitFieldIndex : une puissance de 2 permettant de savoir le membre est chargé (pour le cas d'un chargement partiel)

Pour résumé à quoi sert ce BitFieldIndex, chaque champs possède une puissance de 2 comme index : 1, 2, 4, 8,16... Au sein de l'entité, une variable permet de savoir quels champs sont chargés ou non grâce à une combinaison de ces puissances. Par exemple si cette variable vaut 11, alors cela signifie que les champs d'index 1, 2 et 8 sont chargés. L'appel au un champ d'index 4 déclenchera dans ce cas un reload complet de l'objet.

 

Sans plus attendre et pour conclure, le code au combien compliqué de cet attribut suivi de son utilisation dans une autre classe :

  1:     [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
  2:     public class MappingAttribute : Attribute
  3:     {
  4:         private string _columnName;
  5:         private ulong _bitFieldIndex;
  6:         public MappingAttribute()
  7:         {
  8:         }
  9:         public string ColumnName
 10:         {
 11:             get { return _columnName; }
 12:             set { _columnName = value; }
 13:         }
 14:         public ulong BitFieldIndex
 15:         {
 16:             get { return _bitFieldIndex; }
 17:             set { _bitFieldIndex = value; }
 18:         }
 19:     }
 20: 
  1:     public class News : Entity<int>
  2:     {
  3:         #region Mapped Fields
  4: 
  5:         private enum MF
  6:         {
  7:             Id = 1,
  8:             Name = 2,
  9:             Content = 4,
 10:             CreationDate = 8,
 11:             ModificationDate = 16
 12:         }
 13: 
 14:         #endregion
 15: 
 16:         #region Private Members
 17: 
 18:         [MappingAttribute(ColumnName = "NewsId", BitFieldIndex = (ulong)MF.Id)]
 19:         private int _id;
 20: 
 21:         [MappingAttribute(ColumnName = "Name", BitFieldIndex = (ulong)MF.Name)]
 22:         private string _name;
 23: 
 24:         [MappingAttribute(ColumnName = "Content", BitFieldIndex = (ulong)MF.Content)]
 25:         private string _content;
 26: 
 27:         [MappingAttribute(ColumnName = "CreationDate", BitFieldIndex = (ulong)MF.CreationDate)]
 28:         private DateTime _creationDate;
 29: 
 30:         [MappingAttribute(ColumnName = "ModificationDate", BitFieldIndex = (ulong)MF.ModificationDate)]
 31:         private DateTime _modificationDate;
 32: 
 33:         #endregion
 34: 
 35:     }
 36: 

 

Petite dédicace à bibi : Ne pars pas !

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:     }
   9:  
  10:     public static T GetInstance()
  11:     {
  12:         return _singleton;
  13:     }
  14: }

Le code indique que la classe prendra un argument en générique qui, grâce à la clause where, devra être une classe, et avoir un constructeur public par défaut. Le reste du code est identique au code présenté précédemment. L'appel quant à lui change un peu, il faut désormais appeler la méthode DoThings de cette manière :

   1: Singleton<MyClass>.GetInstance().DoThings();

Je le concède, cela prends plus de caractères pour faire l'appel, cependant cela devient beaucoup plus simple lors de l'écriture des classes. En plus on peut le customiser pour posséder plusieurs singleton d'une même classe. Par exemple, imaginons un système qui impose d'aller taper une base de données dans un cas, et une autre base de données dans un autre ; il nous faudrait deux singletons, un pour chaque base de données.

Pour cela on peut définir une seconde classe Singleton qui permettrait d'obtenir un singleton en fonction d'un paramètre fourni lors de l'appel à GetInstance. Sans plus attendre, le code qui permet ça :

   1: public static class Singleton<T, P> where T : class, new()
   2: {
   3:     private static Dictionary<P, T> _singleton;
   4:     private static ReaderWriterLockSlim _lock;
   5:  
   6:     static Singleton()
   7:     {
   8:         _singleton = new Dictionary<P, T>();
   9:         _lock = new ReaderWriterLockSlim();
  10:     }
  11:  
  12:     public static T GetInstance(P param)
  13:     {
  14:         T t;
  15:         _lock.EnterUpgradeableReadLock();
  16:         try
  17:         {
  18:             if (!_singleton.TryGetValue(param, out t))
  19:             {
  20:                 _lock.EnterWriteLock();
  21:                 try
  22:                 {
  23:                     if (!_singleton.ContainsKey(param))
  24:                     {
  25:                         t = new T();
  26:                         _singleton.Add(param, t);
  27:                     }
  28:                 }
  29:                 finally
  30:                 {
  31:                     _lock.ExitWriteLock();
  32:                 }
  33:             }
  34:         }
  35:         finally
  36:         {
  37:             _lock.ExitUpgradeableReadLock();
  38:         }
  39:         return t;
  40:     }
  41: }

Le code ce coup-ci a pas mal changé. Tout d'abord on voit apparaître un dictionnaire qui va contenir tous nos singletons en fonction d'une clé qui sera le paramètre fourni lors de l'appel à GetInstance. On reconnait aussi le ReaderWriterLockSlim qui est là pour éviter les collisions de threads lorsque deux threads concurrents essaieront d'obtenir un même singleton qui n'existe pas encore.

Normalement le code devrait être Thread-Safe, mais j'avoue que je n'ai fait aucune charge pour m'en assurer.

Pour obtenir un singleton paramétré, on fait désormais cet appel :

   1: Singleton<MyClass, int>.GetInstance(1).DoThings(); 
   2: Singleton<MyClass, int>.GetInstance(2).DoThings();

Ces deux lignes utiliseront deux instances distinctes de la classe MyClass.

Une partie de Cache - Cache / Partie 3

Voilà la dernière partie de cette aventure au combien trépidante. Il ne reste plus qu'à vider régulièrement le contenu du cache pour que ce dernier soit complet.

Pour faire cela, nous allons utiliser un Timer (namespace System.Timers). Pour ceux qui ne connaîtraient pas cette classe, peu nombreux j'imagine, elle permet de déclencher un événement à intervalle régulier.

Voici très brièvement sa définition au sein de notre classe :

   1: private Timer _timer;
   2:  
   3: public Cache()
   4: {
   5:     _timer = new Timer();
   6:     _timer.Elapsed += timer_Elapsed;
   7:     _timer.Interval = 5000;
   8:     _timer.Start();
   9:  
  10: ...
  11: }

Le principe est super simple : lorsque l'événement se déclenche, on regarde tous les éléments présents et on efface ceux qui sont trop vieux. Cependant, avant de rentrer plus en détails dans le vif du sujet, je me dois d'exposer une dernière petite chose.

Dans ce cache, nous allons offrir la possibilité d'être notifié lorsqu'un élément est supprimé du cache et parce qu'on est sympa, on va aussi offrir la possibilité d'empêcher que l'élément soit supprimé. Là d'un coup tu te dis : c'est con ce que tu dis, le cache n'a plus aucun intérêt. Hé bien tu n'as qu'à moitié tord !  Je dis beaucoup de conneries, mais je dis aussi parfois des trucs qui peuvent servir.

Par exemple, tu es un site marchand et tu souhaites mettre en cache les paniers de tes utilisateurs. Ces paniers ne doivent subsister que 20 min (histoire de leur mettre la pression pour qu'ils achètent vite :p) mais attention tu es tombé sur un mec un peu lent qui a un panier qui vient d'atteindre les 20 min et il est en train de payer, donc ce serait dommage de lui effacer son panier. Il faut donc pouvoir offrir la possibilité de garder le panier au-delà de 20 min qui est pourtant sa durée de vie... Ok, c'est pas terrible comme exemple, mais ça illustre un peu le concept.

Pour faire tout ça nous allons créer nos propres EventArgs, delegate et event. En voici les définitions :

   1: public class ItemRemovedEventArgs<K, V> : EventArgs
   2: {
   3:     #region Private Members
   4:  
   5:     private V _value;
   6:     private K _key;
   7:     private bool _cancel;
   8:  
   9:     #endregion
  10:  
  11:     #region Constructors
  12:  
  13:     public ItemRemovedEventArgs(K key, V value)
  14:     {
  15:         _key = key;
  16:         _value = value;
  17:         _cancel = false;
  18:     }
  19:  
  20:     #endregion
  21:  
  22:     #region Properties
  23:  
  24:     public K Key
  25:     {
  26:         get { return _key; }
  27:     }
  28:  
  29:     public V Value
  30:     {
  31:         get { return _value; }
  32:     }
  33:  
  34:     public bool Cancel
  35:     {
  36:         get { return _cancel; }
  37:         set { _cancel = value; }
  38:     }
  39:  
  40:     #endregion
  41: }

et

   1: public delegate void ItemRemovedHandler(object sender, ItemRemovedEventArgs<K, V> e); 
   2:  
   3: public event ItemRemovedHandler ItemRemoved;

 

Bon, fini les plaisanteries, maintenant on veut voir du code ! Voici le code qui s'occupe de la suppression tel qu'il est présent dans mon projet :

   1: private void timer_Elapsed(object sender, ElapsedEventArgs e)
   2: {
   3:     // If a scavenging is currently being processed,
   4:     // don't try to do one too to avoid a longer lock on the dictionnary
   5:     if (!_isScavenging)
   6:     {
   7:         _isScavenging = true;
   8:  
   9:         // Check for old entries
  10:         _lock.EnterUpgradeableReadLock();
  11:         try
  12:         {
  13:             List<K> keysToDelete = new List<K>();
  14:             DateTime currentDatetime = DateTime.Now;
  15:  
  16:             // TODO: Change the foreach to a while for better performance
  17:             foreach (KeyValuePair<K, CacheItem> pair in _items)
  18:             {
  19:                 if ((pair.Value.LastRefresh + pair.Value.Duration) < currentDatetime)
  20:                 {
  21:                     keysToDelete.Add(pair.Key);
  22:                 }
  23:             }
  24:             if (keysToDelete.Count > 0)
  25:             {
  26:                 _lock.EnterWriteLock();
  27:                 try
  28:                 {
  29:                     for (int j = 0; j < keysToDelete.Count; j++)
  30:                     {
  31:                         V value = _items[keysToDelete[j]].Item;
  32:                         if (ItemRemoved != null)
  33:                         {
  34:                             ItemRemovedEventArgs<K, V> ea = new ItemRemovedEventArgs<K, V>(keysToDelete[j], value);
  35:                             ItemRemoved(this, ea);
  36:                             if (!ea.Cancel)
  37:                             {
  38:                                 _items.Remove(keysToDelete[j]);
  39:                             }
  40:                         }
  41:                         else
  42:                         {
  43:                             _items.Remove(keysToDelete[j]);
  44:                         }
  45:                     }
  46:                 }
  47:                 finally
  48:                 {
  49:                     _lock.ExitWriteLock();
  50:                 }
  51:             }
  52:         }
  53:         finally
  54:         {
  55:             _lock.ExitUpgradeableReadLock();
  56:         }
  57:  
  58:         _isScavenging = false;
  59:     }
  60: }

 

Petite remarque qui a je trouve son importance, on fait toute cette manip en deux temps :

  1. Je recherche tous les éléments que je dois supprimer
  2. Je supprime les éléments

Pourquoi faire la chose en deux fois ? Tout d'abord parce qu'un Dictionary ne permet pas de supprimer un élément lorsque l'on est en train de parcourir sa collection (avec un foreach), mais surtout aussi, afin de ne locker la collection qu'en lecture au début, et n'acquérir le lock en écriture que si il y a besoin de faire des modifications. Grâce à cela on permet des locks plus brefs.

 

Et voilà, si je ne me suis pas vautré dans mes explications, tout cela devrait permettre d'avoir un cache qui fonctionne plutôt pas mal. Personnellement j'ai effectué quelques tests afin de vérifier le bon fonctionnement de ce code (oui je vais éviter de raconter des conneries si possible) et mon code a mis sur mon pc environ 13 secondes pour faire 12 500 000 accès (moitié en lecture, moitié en écriture).

Le protocole étant grossièrement 500 threads qui en parallèle font 25 000 tentatives chacun sur le cache (les lectures et écritures étant réparties aléatoirement sur l'ensemble du test). Si on considère qu'une lecture prends deux fois moins de temps qu'un ajout, et que mes process sont répartis également sur mes dual core, on obtient qu'une lecture prends 0,025 ms et une écriture 0,050 ms. Et tout ça sans aucune erreur ni inter-blocage :-)

Une partie de Cache - Cache / Partie 2

Et nous revoilà pour la seconde partie de ce post consacré au Cache tant décrié ;-) .

Afin de pouvoir stocker des éléments dans notre cache, nous allons devoir encapsuler notre objet Obj dans une classe générique (private au sein de la classe de Cache) qui contiendra la dernière date d'accès à Obj ainsi que sa durée de vie dans le cache. En voici le descriptif :

   1: private class CacheItem
   2: {
   3:     public V Item;
   4:     public DateTime LastRefresh;
   5:     public readonly bool SlidingExpiration;
   6:     public readonly TimeSpan Duration;
   7:  
   8:     public CacheItem(V value, bool slidingExpiration, TimeSpan duration)
   9:     {
  10:         Item = value;
  11:         LastRefresh = DateTime.Now;
  12:         SlidingExpiration = slidingExpiration;
  13:         Duration = duration;
  14:     }
  15: }

Si on se souvient du post précédent on peut voir rapidement que l'on reprend le type générique fourni dans la définition de notre cache (ie. Cache<K, V>).

On peut se demander pourquoi ne pas stocker directement la date d'expiration de l'objet plutôt que la date du dernier accès. Tout simplement parce que notre cache permet une SlidingExpiration (ie. prolonger la durée de vie de l'objet à chaque accès chaque fois que ce dernier est requêté). Cette SlidingExpiration imposerait de recalculer continuellement une nouvelle date d'expiration ce qui coûterait cher en terme de temps processeur.

Dans le post précédent, j'avais indiqué que notre cache allait implémenter l'interface IDictionnary. Or cette interface ne prévoit pas dans ses méthodes un argument définissant une durée de vie, ni la SlidingExpiration. Il faut donc pouvoir définir des valeurs par défaut lorsque ces méthodes seront appelées. Cela sera possible grâce aux propriétés suivante dans notre cache.

   1: public TimeSpan DefaultDuration
   2: {
   3:     get { return _defaultDuration; }
   4:     set { _defaultDuration = value; }
   5: }
   6:  
   7: public bool DefaultSlidingExpiration
   8: {
   9:     get { return _defaultSlidingExpiration; }
  10:     set { _defaultSlidingExpiration = value; }
  11: }

 

Mettre ici l'ensemble des méthodes que les différentes interfaces nous imposent d'implémenter nous prendrait un peu trop de place, et surtout je vais pas faire tout le boulot... :-) Je me contenterai seulement de mettre deux exemples (l'ajout et la récupération de valeur) qui illustre tout ce dont j'ai déjà parlé (le ReaderWriterLockSlim ainsi que notre classe générique) :

   1: public void Add(K key, V value)
   2: {
   3:     _lock.EnterWriteLock();
   4:     try
   5:     {
   6:         _items.Add(key, new CacheItem(value, _defaultSlidingExpiration, _defaultDuration));
   7:     }
   8:     finally
   9:     {
  10:         _lock.ExitWriteLock();
  11:     }
  12: }
  13:  
  14: ...
  15:  
  16: public bool TryGetValue(K key, out V value)
  17: {
  18:     _lock.EnterReadLock();
  19:     try
  20:     {
  21:         CacheItem ci;
  22:         if (_items.TryGetValue(key, out ci))
  23:         {
  24:             if (ci.SlidingExpiration)
  25:             {
  26:                 ci.LastRefresh = DateTime.Now;
  27:             }
  28:             value = ci.Item;
  29:             return true;
  30:         }
  31:     }
  32:     finally
  33:     {
  34:         _lock.ExitReadLock();
  35:     }
  36:     value = default(V);
  37:     return false;
  38: }

Et voilà, grâce à tout ça, et surtout si tu es motivé, tu devrais être capable d'avoir un cache qui se remplit sans problème mais qui est aussi et surtout entièrement Thread-Safe.

 

Dans un dernier post, nous verrons ensemble la touche finale de notre cache : comment faire expirer les éléments de notre cache.

Une partie de Cache - Cache / Partie 1

Dans tout bon site web, il est je pense très important de monter des informations en cache afin de limiter les aller-retour entre les serveurs web et la base de données. Je dirai même qu'il s'agit de la pierre angulaire de tout site internet qui est supposé tenir la charge. Je me propose donc de réaliser un petit cache au travers de 3 posts dont voici le premier. Le cache qui sera présenté ici devra donc pouvoir contenir des éléments (c'est un peu la base), faire expirer les éléments qui sont à l'intérieur et surtout être Thread-Safe !!!

Comme je l'indiquai dans le premier post, je compte "partager" mes développements avec ceux qui me liront, ceux qui me diront que c'est n'importe quoi ce que je raconte, mais aussi ceux, dont le nombre tends vers 0, qui me considèreront comme un dieu (l'espoir fait vivre n'est-ce pas ?) . C'est pourquoi il s'agira de notre code, notre Cache.

Comme dans beaucoup d'autres classe que nous écrirons, nous allons utiliser les Generics afin de typer notre cache. Cela nous permettra d'éviter de devoir caster nos object, mais aussi nous évitera le boxing / unboxing, ce qui devrait théoriquement améliorer aussi les performances.

Voici la première description de notre classe :

   1: namespace JALUM.Utility.Cache
   2: {
   3:     public class Cache<K, V> : IDictionary<K, V>, IDisposable
   4:     {
   5:     }
   6: }

Comme tu peux le voir, cette dernière agira exactement comme un dictionnaire Key / Value.

Afin de vérifier quels éléments doivent être conservés dans le cache, et lesquels doivent être supprimer, nous allons utiliser un Timer, par conséquent nous implémenterons aussi l'interface IDisposable afin de libérer correctement toutes les ressources.

Nous allons nous contenter, à peu de choses près, d'encapsuler un Dictionnary<K, V> en le rendant Thread-Safe. La méthode la plus simple serait d'utiliser le mot clé lock fourni par le framework .net mais cette utilisation a deux principaux inconvénients :

  • Elle peut lorsqu'elle est mal utilisée conduire à des Dead-Locks. Si je lock A pour traiter B qui elle même tente de locker A -> l'appli freeze...
  • Elle induit des locks lorsque cela n'est pas nécessaire. Si deux threads tentent d'accéder en lecture à la même ressource, le mot clé lock bloque le second thread jusqu'à ce que le premier ait terminé. Or les deux threads auraient pu y accéder en même temps sans problèmes.

La seconde méthode, celle que nous utiliserons, est d'utiliser un ReaderWriterLock, ou sa nouvelle version dans le Framework 3.5 : ReaderWriterLockSlim, qui permet de différencier les locks dédiés à la lecture des locks dédiés à l'écriture. Grosso modo, on signale que l'on souhaite faire une lecture. Si aucun lock en écriture n'est posé, on accède directement à la ressource, peu importe si d'autres sont aussi en train d'y accéder en lecture ; si un lock en écriture est déjà posé, le thread attends que le thread en écriture ait terminé son boulot. Lorsque l'on signale vouloir acquérir un lock en écriture, le thread attends que les thread en lecture déjà en cours se terminent. Si de nouveau locks en lecture sont demandés, ils attendront que le thread ait obtenu son lock et ai pu faire son boulot avant d'avoir à leur tour le droit d'accéder à la ressource.

Son utilisation est relativement simple mais comme les geeks que nous sommes, mieux vaut pour nous 20 lignes de code que 5 lignes d'explications ...

La lecture :

   1: _lock.EnterReadLock();
   2: try
   3: {
   4:  
   5: }
   6: finally
   7: {
   8:     _lock.ExitReadLock();
   9: }

L'écriture :

   1: _lock.EnterWriteLock(); 
   2: try 
   3: { 
   4:  
   5: } 
   6: finally 
   7: { 
   8:     _lock.ExitWriteLock(); 
   9: }

L'upgrade de la lecture en écriture (cela consomme moins que relâcher le lock en lecture et en acquérir un nouveau en écriture) :

   1: _lock.EnterUpgradeableReadLock(); 
   2: try 
   3: {  
   4:     _lock.EnterWriteLock(); 
   5:     try 
   6:     { 
   7:     } 
   8:     finally 
   9:     { 
  10:         _lock.ExitWriteLock(); 
  11:     } 
  12: } 
  13: finally 
  14: { 
  15:     _lock.ExitUpgradeableReadLock(); 
  16: }

Voila pour la première partie de cette magnifique classe. A bientôt pour la suite.

Amis du jour, BONJOUR !

Après une première nuit chaotique et des problèmes de connexion internet, j'ai laissé un premier post quelque peu léger, voir très léger... je le modifie donc afin de mieux me présenter.

Tout d'abord une réponse rapide à la question "Qui est-ce ?". Comme indiqué plus haut je suis Luc Dubrois. Je travaille dans une SSII nommée Winwise et je suis actuellement en mission chez un grand site marchand qui propose des ventes événementielles mais dont je dois taire le nom... Chez ce client, qui est presque le seul chez qui j'ai été depuis que je travaille, j'ai participé à la refonte technique de leur frontal web.

Ensuite pourquoi ouvrir un blog ? Par ce blog j'aimerai partager mes interrogations, découvertes, développements avec d'autres personnes passionnées et assez courageuses pour me lire.

Ceux qui me connaissent savent que cela fait maintenant 2 ans que je parle d'un projet, du doux nom de JALUM, et je me suis dit en mon fort intérieur "Putain il faut que j'aille dispenser la pensée JALUM à travers le monde" (joke inside... désolé) en publiant sur ce blog les différentes étapes, petits bouts de code qui mis les uns avec les autres formeront ce magnifique projet qu'est JALUM.

Le pari que je me lance aujourd'hui est de faire vivre ce blog en y écrivant "relativement" souvent. De ce fait, et pour ne pas raconter ma magnifique journée au travail, je serai obliger d'avancer sur ce projet, qui peut-être un jour sera vraiment en ligne, et de narrer les tribulations qui seront les miennes dans ce développement.

Ce que je compte proposer comme code dans ce blog n'est certainement pas la manière la plus "jolie" de faire les choses, mais elle aura au moins le mérite, normalement, de fonctionner. Entre chaque post il sera aussi possible que le code ait évolué (notamment au niveau des signatures de méthodes) mais de manière générale les concepts resteront les mêmes.

Enfin pour terminer cette petite introduction à ce que sera ce blog, je me dois de préciser que les posts qui seront proposés seront de manière beaucoup plus orienté web que WinForms, et donc je m'attacherai tout particulièrement aux problématiques rencontrées avec cela, à savoir un mode déconnecté, un contexte différent à chaque requête, une basse consommation de ressources pour les montées en charge, l'accès à des ressources partagées...

Sur ce, bonne journée à tous et encore désolé pour le premier post très léger de cette nuit !


Les 10 derniers blogs postés

- Office 365: Script PowerShell pour assigner des droits Full Control à un groupe défini par Blog Technique de Romelard Fabrice le 04-30-2017, 09:22

- SharePoint 20XX: Script PowerShell pour exporter en CSV toutes les listes d’une ferme pour auditer le contenu avant migration par Blog Technique de Romelard Fabrice le 03-28-2017, 17:53

- Les pièges de l’installation de Visual Studio 2017 par Blog de Jérémy Jeanson le 03-24-2017, 13:05

- UWP or not UWP sur Visual Studio 2015 ? par Blog de Jérémy Jeanson le 03-08-2017, 19:12

- Désinstallation de .net Core RC1 Update 1 ou SDK de Core 1 Preview 2 par Blog de Jérémy Jeanson le 03-07-2017, 19:29

- Office 365: Ajouter un utilisateur ou groupe dans la liste des Site collection Administrator d’un site SharePoint Online via PowerShell et CSOM par Blog Technique de Romelard Fabrice le 02-24-2017, 18:52

- Office 365: Comment créer une document library qui utilise les ContentTypeHub avec PowerShell et CSOM par Blog Technique de Romelard Fabrice le 02-22-2017, 17:06

- [TFS] Supprimer en masse les dépendances à SQL Enterprise ou Developer avant de procéder à une migration par Blog de Jérémy Jeanson le 02-20-2017, 20:30

- Office 365: Attention au volume utilisé par les fichiers de Thèmes de SharePoint Online par Blog Technique de Romelard Fabrice le 02-07-2017, 18:19

- [SCVMM] Supprimer une machine bloquée par Blog de Jérémy Jeanson le 01-31-2017, 21:22