Object to Object Mapper, Part 3

2/12/2010

In the last post in this series, I showed my second pass (or would you consider it a first pass since the original was more a factory pattern?) at an object to object mapper. It was rather basic and was missing a number of features which would make it really useable (type conversion, string formatting, etc.). Not to mention much of the code that I was working on needed to be moved into my utility library and cleaned up. Well now I'm back to give my third pass at it:

   1: /// <summary>
   2: /// Maps two objects together
   3: /// </summary>
   4: /// <typeparam name="Source">Source type</typeparam>
   5: /// <typeparam name="Destination">Destination type</typeparam>
   6: public class Mapping<Source,Destination>:IMapping
   7: {
   8:  
   9:     #region Constructor
  10:  
  11:     /// <summary>
  12:     /// Constructor
  13:     /// </summary>
  14:     public Mapping()
  15:     {
  16:         Setup();
  17:     }
  18:  
  19:     #endregion
  20:  
  21:     #region IMapping Members
  22:  
  23:     public Type SourceType { get { return typeof(Source); } }
  24:  
  25:     public Type DestinationType { get { return typeof(Destination); } }
  26:  
  27:     public void Sync(object SourceObject, object DestinationObject)
  28:     {
  29:         if (SourceObject.GetType() == SourceType)
  30:         {
  31:             for (int y = 0; y < Mappings.Count; ++y)
  32:             {
  33:                 object SourceValue = Utilities.Reflection.GetPropertyValue(SourceObject, Mappings[y].Source);
  34:                 Utilities.Reflection.SetValue(SourceValue, DestinationObject, Mappings[y].Destination, Mappings[y].Format);
  35:             }
  36:             return;
  37:         }
  38:         for (int y = 0; y < Mappings.Count; ++y)
  39:         {
  40:             object DestinationValue = Utilities.Reflection.GetPropertyValue(SourceObject, Mappings[y].Destination);
  41:             Utilities.Reflection.SetValue(DestinationValue, DestinationObject, Mappings[y].Source, Mappings[y].Format);
  42:         }
  43:     }
  44:  
  45:     public object Create(object Source)
  46:     {
  47:         if (Source.GetType() == SourceType)
  48:         {
  49:             object DestinationObject = typeof(Destination).Assembly.CreateInstance(typeof(Destination).FullName);
  50:             Sync(Source, DestinationObject);
  51:             return DestinationObject;
  52:         }
  53:         object SourceObject = typeof(Destination).Assembly.CreateInstance(typeof(Source).FullName);
  54:         Sync(Source, SourceObject);
  55:         return SourceObject;
  56:     }
  57:  
  58:     #endregion
  59:  
  60:     #region Protected Functions
  61:  
  62:     /// <summary>
  63:     /// Maps a source property to a destination property
  64:     /// </summary>
  65:     /// <param name="SourceExpression">Source property</param>
  66:     /// <param name="DestinationExpression">Destination property</param>
  67:     protected void Map(Expression<Func<Source, object>> SourceExpression,
  68:         Expression<Func<Destination, object>> DestinationExpression)
  69:     {
  70:         Map(SourceExpression, DestinationExpression, "");
  71:     }
  72:  
  73:     /// <summary>
  74:     /// Maps a source property to a destination property
  75:     /// </summary>
  76:     /// <param name="SourceExpression">Source property</param>
  77:     /// <param name="DestinationExpression">Destination property</param>
  78:     /// <param name="Format">The string format that the destination should use</param>
  79:     protected void Map(Expression<Func<Source, object>> SourceExpression,
  80:         Expression<Func<Destination, object>> DestinationExpression,
  81:         string Format)
  82:     {
  83:         Setup();
  84:         string SourceName = Utilities.Reflection.GetPropertyName<Source>(SourceExpression);
  85:         string DestinationName = Utilities.Reflection.GetPropertyName<Destination>(DestinationExpression);
  86:         for (int x = 0; x < Mappings.Count; ++x)
  87:         {
  88:             if (Mappings[x].Destination == DestinationName)
  89:             {
  90:                 Mappings.RemoveAt(x);
  91:                 break;
  92:             }
  93:         }
  94:         for (int x = 0; x < Mappings.Count; ++x)
  95:         {
  96:             if (Mappings[x].Source == SourceName)
  97:             {
  98:                 Mappings.RemoveAt(x);
  99:                 break;
 100:             }
 101:         }
 102:         Mappings.Add(new MappingInfo(SourceName, DestinationName, Format));
 103:     }
 104:  
 105:     /// <summary>
 106:     /// Removes a mapping
 107:     /// </summary>
 108:     /// <param name="SourceExpression">Source property to ignore</param>
 109:     protected void Ignore(Expression<Func<Source, object>> SourceExpression)
 110:     {
 111:         Setup();
 112:         string SourceName = Utilities.Reflection.GetPropertyName<Source>(SourceExpression);
 113:         for (int x = 0; x < Mappings.Count; ++x)
 114:         {
 115:             if (Mappings[x].Source == SourceName)
 116:             {
 117:                 Mappings.RemoveAt(x);
 118:                 break;
 119:             }
 120:         }
 121:     }
 122:  
 123:     /// <summary>
 124:     /// Removes a mapping
 125:     /// </summary>
 126:     /// <param name="DestinationExpression">Destination property to ignore</param>
 127:     protected void Ignore(Expression<Func<Destination, object>> DestinationExpression)
 128:     {
 129:         Setup();
 130:         string DestinationName = Utilities.Reflection.GetPropertyName<Destination>(DestinationExpression);
 131:         for (int x = 0; x < Mappings.Count; ++x)
 132:         {
 133:             if (Mappings[x].Destination == DestinationName)
 134:             {
 135:                 Mappings.RemoveAt(x);
 136:                 break;
 137:             }
 138:         }
 139:     }
 140:  
 141:     #endregion
 142:  
 143:     #region Private Functions
 144:  
 145:     /// <summary>
 146:     /// Sets up the mapping
 147:     /// </summary>
 148:     public void Setup()
 149:     {
 150:         if (Mappings == null)
 151:         {
 152:             Mappings = new List<MappingInfo>();
 153:             PropertyInfo[] Properties = typeof(Source).GetProperties();
 154:             Type DestinationType = typeof(Destination);
 155:             for (int x = 0; x < Properties.Length; ++x)
 156:             {
 157:                 if (DestinationType.GetProperty(Properties[x].Name) != null)
 158:                 {
 159:                     Mappings.Add(new MappingInfo(Properties[x].Name, Properties[x].Name, ""));
 160:                 }
 161:             }
 162:         }
 163:     }
 164:  
 165:     #endregion
 166:  
 167:     #region Protected Variables
 168:  
 169:     internal List<MappingInfo> Mappings { get; set; }
 170:  
 171:     #endregion
 172: }

The mapping class was described in the previous post and it works in a similar fashion. It's a base class that needs to be implemented, the manager class goes looking for it in the assembly that you specify, etc. In the constructor, you would still declare your mappings but this time you can now specify a formatting for strings. For instance I could have a DateTime map to a string and have it formatted however you wanted by calling Map(DateTimeProperty,StringProperty,"THE FORMAT I WANT"). Obviously not that exactly as you probably don't have those properties and that format string wouldn't work at all... Past that the mappings are no longer a simple Dictionary of source/destination paths. Instead it's a MappingInfo class, which is really just the source/destination paths and another string that holds the formatting. Otherwise the setup is about the same.

Well there is a slight change to the setup code. Namely they now call Utilities.Reflection.GetPropertyName. It's the same as the GetName function from the previous post, just moved into my utility library. Now where the changes start rolling in is the Sync function and a new function called Create. In Sync, the first thing you may notice is the fact that it checks to see if the Source object is actually of type Source. The reason for this is really to make my life easier. In my first attempt, it only synced from the Source to the Destination. This was great if you were only sending data one way, but if you wanted to sync the other way you had to set up another mapping class. Sort of kills the whole less coding/making your life easier idea. So the manager now just checks if the source/destination match up (in either order) and sends in the objects. As such the Mapping class needs to figure out which direction it's going. Once the direction is established it calls Utilities.Reflection.GetPropertyValue, which can be found below:

   1: /// <summary>
   2: /// Gets a property's value
   3: /// </summary>
   4: /// <param name="SourceObject">object who contains the property</param>
   5: /// <param name="PropertyPath">Path of the property (ex: Prop1.Prop2.Prop3 would be
   6: /// the Prop1 of the source object, which then has a Prop2 on it, which in turn
   7: /// has a Prop3 on it.)</param>
   8: /// <returns>The value contained in the property or null if the property can not
   9: /// be reached</returns>
  10: public static object GetPropertyValue(object SourceObject, string PropertyPath)
  11: {
  12:     if (SourceObject == null||string.IsNullOrEmpty(PropertyPath))
  13:         return null;
  14:     string[] Splitter = { "." };
  15:     string[] SourceProperties = PropertyPath.Split(Splitter, StringSplitOptions.None);
  16:     object TempSourceProperty = SourceObject;
  17:     Type PropertyType = SourceObject.GetType();
  18:     for (int x = 0; x < SourceProperties.Length; ++x)
  19:     {
  20:         PropertyInfo SourcePropertyInfo = PropertyType.GetProperty(SourceProperties[x]);
  21:         if (SourcePropertyInfo == null)
  22:             return null;
  23:         TempSourceProperty = SourcePropertyInfo.GetValue(TempSourceProperty, null);
  24:         if (TempSourceProperty == null)
  25:             return null;
  26:         PropertyType = SourcePropertyInfo.PropertyType;
  27:     }
  28:     return TempSourceProperty;
  29: }

In my case, I make no assumptions that you are only going to get shallow properties. For all I know you're going to want something like MyObject.CurrentDate.Month and map that to some Label somewhere. So the function takes a string path (in the example I gave it would actually be "CurrentDate.Month" assuming MyObject was the object and not a property) and simply loops through it trying to get to the property specified and its value. Anyway, the Sync function calls this to get the current source value and then feeds that to the Utilities.Reflection.SetValue function. This function is where it gets a bit more interesting.

   1: /// <summary>
   2: /// Sets the value of destination property
   3: /// </summary>
   4: /// <param name="SourceValue">The source value</param>
   5: /// <param name="DestinationObject">The destination object</param>
   6: /// <param name="PropertyPath">The path to the property (ex: MyProp.SubProp.FinalProp
   7: /// would look at the MyProp on the destination object, then find it's SubProp,
   8: /// and finally copy the SourceValue to the FinalProp property on the destination
   9: /// object)</param>
  10: /// <param name="Format">Allows for formatting if the destination is a string</param>
  11: public static void SetValue(object SourceValue, object DestinationObject, 
  12:     string PropertyPath, string Format)
  13: {
  14:     string[] Splitter = { "." };
  15:     string[] DestinationProperties = PropertyPath.Split(Splitter, StringSplitOptions.None);
  16:     object TempDestinationProperty = DestinationObject;
  17:     Type DestinationPropertyType = DestinationObject.GetType();
  18:     PropertyInfo DestinationProperty = null;
  19:     for (int x = 0; x < DestinationProperties.Length - 1; ++x)
  20:     {
  21:         DestinationProperty = DestinationPropertyType.GetProperty(DestinationProperties[x]);
  22:         DestinationPropertyType=DestinationProperty.PropertyType;
  23:         TempDestinationProperty = DestinationProperty.GetValue(TempDestinationProperty, null);
  24:         if (TempDestinationProperty == null)
  25:             return;
  26:     }
  27:     DestinationProperty = DestinationPropertyType.GetProperty(DestinationProperties[DestinationProperties.Length - 1]);
  28:     SetValue(SourceValue, TempDestinationProperty, DestinationProperty, Format);
  29: }

This function works in a similar fashion to the GetPropertyValue function to get the destination property that we want. This function then calls another version of SetValue:

   1: /// <summary>
   2: /// Sets the value of destination property
   3: /// </summary>
   4: /// <param name="SourceValue">The source value</param>
   5: /// <param name="DestinationObject">The destination object</param>
   6: /// <param name="DestinationPropertyInfo">The destination property info</param>
   7: /// <param name="Format">Allows for formatting if the destination is a string</param>
   8: public static void SetValue(object SourceValue, object DestinationObject,
   9:     PropertyInfo DestinationPropertyInfo, string Format)
  10: {
  11:     if (DestinationObject == null || DestinationPropertyInfo == null)
  12:         return;
  13:     Type DestinationPropertyType = DestinationPropertyInfo.PropertyType;
  14:     DestinationPropertyInfo.SetValue(DestinationObject,
  15:         Parse(SourceValue, DestinationPropertyType, Format),
  16:         null);
  17: }

And this function sets the property's value but you'll notice that there is a call to Parse in the value portion of the PropertyInfo.SetValue function. This is once again, yet another function within the utility library:

   1: private static object Parse(object Input, Type OutputType,string Format)
   2: {
   3:     if (Input==null || OutputType == null)
   4:         return null;
   5:     Type InputType = Input.GetType();
   6:     if (InputType == OutputType)
   7:     {
   8:         return Input;
   9:     }
  10:     else if (OutputType == typeof(string) && !string.IsNullOrEmpty(Format))
  11:     {
  12:         return StringHelper.FormatToString(Input, Format);
  13:     }
  14:     else if (OutputType == typeof(string))
  15:     {
  16:         return Input.ToString();
  17:     }
  18:     else
  19:     {
  20:         return CallMethod("Parse", OutputType.Assembly.CreateInstance(OutputType.FullName), Input.ToString());
  21:     }
  22: }

This function determines if the objects are the same type. If they are, it simply returns the input object. If, however it is a string and the format string isn't null, it calls FormatToString (this is just the FormatToString function from my previous post). If it's a string but the format string is null, it simply calls Input.ToString(). And lastly if they are two different types (for example copying an int to a float), it calls yet another function called CallMethod:

   1: /// <summary>
   2: /// Calls a method on an object
   3: /// </summary>
   4: /// <param name="MethodName">Method name</param>
   5: /// <param name="Object">Object to call the method on</param>
   6: /// <param name="InputVariables">(Optional)input variables for the method</param>
   7: /// <returns>The returned value of the method</returns>
   8: public static object CallMethod(string MethodName, object Object,params object[] InputVariables)
   9: {
  10:     if(string.IsNullOrEmpty(MethodName)||Object==null)
  11:         return null;
  12:     Type ObjectType = Object.GetType();
  13:     MethodInfo Method = null;
  14:     if (InputVariables != null)
  15:     {
  16:         Type[] MethodInputTypes = new Type[InputVariables.Length];
  17:         for (int x = 0; x < InputVariables.Length; ++x)
  18:         {
  19:             MethodInputTypes[x] = InputVariables[x].GetType();
  20:         }
  21:         Method = ObjectType.GetMethod(MethodName, MethodInputTypes);
  22:         if (Method != null)
  23:         {
  24:             return Method.Invoke(Object, InputVariables);
  25:         }
  26:     }
  27:     Method = ObjectType.GetMethod(MethodName);
  28:     if (Method != null)
  29:     {
  30:         return Method.Invoke(Object, null);
  31:     }
  32:     return null;
  33: }

You see, I've been writing a lot of code that would need to call a function of an object using reflection as of late. In this case I can simply tell it the name of the function, send in the object, and any needed input values. The function in turn looks for functions with the specified name that takes the input values (if there are any). If it finds one, it then invokes it, sending in the appropriate values. That's it, but it allows me to do away with rewriting that bit of code every time I need it. Anyway, in this case it's called looking for the Parse function on the object (int.Parse, float.Parse, etc.). It then returns the newly parsed item which then gets set as the property's value. Note that it doesn't do anything special, if you pass in 1.2 and want to convert it to an int, it will fail.

Anyway, that's it to the Sync function (once you parse through the 10 or so layers). The other function is the Create function. This goes back to my need to have this thing act like a factory pattern a bit. All the function does is creates a new instance of either the destination or source object (basically it creates the opposite of what is sent in) and then calls Sync. That's it. So there you go, we now have a simple object to object mapper that does type conversion and string formatting. I'm actually pretty happy with it at this point, although I could speed it up with some lightweight code gen like Gunner Peipman has been doing in the object to object mapper that he has been building. Which by the way you should take a look at if your needs are fairly simple as his implementation is pretty good at this point. Unfortunately it doesn't fit my needs or I'd use it. The next post I'll show the reworked version of my factory/mapper that I was working on in the first post to work with the newer code. So try it out, leave feedback, and happy coding.



Comments

James Craig
February 12, 2010 3:52 PM

Actually right after posting this, I realized that I could switch the CallMethod function call in Parse to this:return CallMethod("Parse", OutputType.Assembly.CreateInstance(OutputType.FullName), StringHelper.FormatToString(Input, Format));This allows me to set a formatting that would allow me to convert from float to int, etc. For example I can set the format string to "f0" and it will convert 3.2 to 3, thus allowing me to save it as an int. I just need it to auto detect those formats for me...