Publié samedi 15 novembre 2008 17:11 par Luke77

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.

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Les 10 derniers blogs postés

- TechDays Paris 2012 : Comment SharePoint a sauvé mes TechDays par Blog Technique de Romelard Fabrice le il y a 8 heures et 49 minutes

- Perspective 3.0 pour Silverlight 5.0 par Perspective le il y a 10 heures et 10 minutes

- TechDays Paris 2012 : Top 10 des Best Practices pour SQL Server par Blog Technique de Romelard Fabrice le il y a 15 heures et 46 minutes

- TechDays Paris 2012 : Kinect + Office 365 un bon geste pour votre SI par Blog Technique de Romelard Fabrice le il y a 16 heures et 9 minutes

- TechDays Paris 2012 : Pleinière du premier jour par Blog Technique de Romelard Fabrice le il y a 16 heures et 25 minutes

- [SharePoint 2010] Erreur d’analyse de contenu “L’élément SharePoint en cours d’analyse a renvoyé une erreur lors de la demande de données auprès du se... par Julien Chable le il y a 20 heures et 58 minutes

- [TechDays2012] Oui j’y serai! par Blog de Jérémy Jeanson le 02-06-2012, 22:13

- TFS Integration Tools – Suivi des synchronisations avec Reporting Services par Vivien Fabing le 02-05-2012, 17:46

- CSS Content State Selectors (Personnal Draft) par Le blog de FremyCompany le 02-04-2012, 15:38

- MBA : Pourquoi faire et comment le choisir ? par Blog Technique de Romelard Fabrice le 02-03-2012, 14:22