Object to Object Mapper, Part 2

1/29/2010

My last post described an odd issue that I ran into with the app I was working on. Basically my first pass at the solution was a object to object mapper/factory pattern/whatever. It's not the most elegant but like I said it was my first pass. But after that I figured why not create a more robust object to object mapper that included that sort of functionality and since I like to reinvent the wheel, I thought what the hell. It turns out for basic functionality, it's quite simple. Anyway, I started with centralizing everything (similar to HaterAide) with a central manager:

   1: public class Manager:Singleton<Manager>
   2: {
   3:     protected Manager()
   4:         : base()
   5:     {
   6:         Mappings = new Dictionary<Type, IMapping>();
   7:     }
   8:  
   9:     public void Setup(List<Assembly> Assemblies)
  10:     {
  11:         foreach (Assembly Assembly in Assemblies)
  12:         {
  13:             Setup(Assembly);
  14:         }
  15:     }
  16:  
  17:     public void Setup(Assembly Assembly)
  18:     {
  19:         List<Type> Types = Utilities.Reflection.GetTypes(Assembly, "IMapping");
  20:         foreach (Type Type in Types)
  21:         {
  22:             IMapping Mapping=(IMapping)Assembly.CreateInstance(Type.FullName);
  23:             Mappings.Add(Mapping.SourceType, Mapping);
  24:         }
  25:     }
  26:  
  27:     public void Sync(object Source, object Destination)
  28:     {
  29:         Mappings[Source.GetType()].Sync(Source, Destination);
  30:     }
  31:  
  32:     private Dictionary<Type, IMapping> Mappings { get; set; }
  33: }

The singleton class is a helper from my utility library and while I know that singletons can cause issues, in this case it makes sense (mostly anyway as I want all of the various mappings in one location). The manager class has a couple setup functions (single assembly or a list of them) and simply creates an instance of anything that has the IMapping interface, which looks like this:

   1: public interface IMapping
   2: {
   3:     Type SourceType { get; }
   4:     Type DestinationType { get; }
   5:     void Sync(object Source,object Destination);
   6: }

The IMapping interface is rather basic and only exposes a couple of items (the type of the source item, the type of the destination item, and a sync function which only goes from source to destination). Going back to the manager, the only other function of note is Sync which simply calls the IMapping object's Sync function. Now for the more interesting item, the actual mapping class:

   1: public class Mapping<Source, Destination>:IMapping
   2: {
   3:     public Mapping()
   4:     {
   5:         Setup();
   6:     }
   7:  
   8:     public void Sync(object SourceObject, object DestinationObject)
   9:     {
  10:         foreach(string Key in Mappings.Keys)
  11:         {
  12:             string[]Splitter={"."};
  13:             string[]SourceProperties=Key.Split(Splitter,StringSplitOptions.None);
  14:             object TempSourceProperty = SourceObject;
  15:             Type PropertyType=SourceType;
  16:             for (int x = 0; x < SourceProperties.Length; ++x)
  17:             {
  18:                 TempSourceProperty = PropertyType.GetProperty(SourceProperties[x]).GetValue(TempSourceProperty, null);
  19:                 PropertyType = TempSourceProperty.GetType();
  20:             }
  21:             string[] DestinationProperties = Mappings[Key].Split(Splitter, StringSplitOptions.None);
  22:             object TempDestinationProperty = DestinationObject;
  23:             Type DestinationPropertyType = DestinationType;
  24:             for (int x = 0; x < DestinationProperties.Length-1; ++x)
  25:             {
  26:                 TempDestinationProperty = DestinationPropertyType.GetProperty(DestinationProperties[x]).GetValue(TempDestinationProperty, null);
  27:                 PropertyType = TempSourceProperty.GetType();
  28:             }
  29:             DestinationPropertyType.GetProperty(DestinationProperties[DestinationProperties.Length-1]).SetValue(TempDestinationProperty,TempSourceProperty,null);
  30:         }
  31:     }
  32:  
  33:     protected void Map(Expression<Func<Source, object>> SourceExpression,
  34:         Expression<Func<Destination, object>> DestinationExpression)
  35:     {
  36:         Setup();
  37:         string SourceName = GetName<Source>(SourceExpression);
  38:         string DestinationName = GetName<Destination>(DestinationExpression);
  39:         if (Mappings.ContainsValue(DestinationName))
  40:         {
  41:             foreach (string Key in Mappings.Keys)
  42:             {
  43:                 if (Mappings[Key] == DestinationName)
  44:                 {
  45:                     Mappings.Remove(Key);
  46:                     break;
  47:                 }
  48:             }
  49:         }
  50:         if (Mappings.ContainsKey(SourceName))
  51:         {
  52:             Mappings[SourceName] = DestinationName;
  53:         }
  54:         else
  55:         {
  56:             Mappings.Add(SourceName, DestinationName);
  57:         }
  58:     }
  59:  
  60:     protected void Ignore(Expression<Func<Source, object>> SourceExpression)
  61:     {
  62:         Setup();
  63:         string SourceName = GetName<Source>(SourceExpression);
  64:         if (Mappings.ContainsKey(SourceName))
  65:         {
  66:             Mappings.Remove(SourceName);
  67:         }
  68:     }
  69:  
  70:     private void Setup()
  71:     {
  72:         if (Mappings == null)
  73:         {
  74:             Mappings = new Dictionary<string, string>();
  75:             PropertyInfo[] Properties = typeof(Source).GetProperties();
  76:             Type DestinationType = typeof(Destination);
  77:             for (int x = 0; x < Properties.Length; ++x)
  78:             {
  79:                 if (DestinationType.GetProperty(Properties[x].Name) != null)
  80:                 {
  81:                     Mappings.Add(Properties[x].Name, Properties[x].Name);
  82:                 }
  83:             }
  84:         }
  85:     }
  86:  
  87:     private string GetName<T>(Expression<Func<T, object>> expression)
  88:     {
  89:         string Name = "";
  90:         if (expression.Body.NodeType == ExpressionType.Convert)
  91:         {
  92:             Name = expression.Body.ToString().Replace("Convert(", "").Replace(")", "");
  93:             Name = Name.Remove(0, Name.IndexOf(".") + 1);
  94:         }
  95:         else
  96:         {
  97:             Name = expression.Body.ToString();
  98:             Name = Name.Remove(0, Name.IndexOf(".") + 1);
  99:         }
 100:         return Name;
 101:     }
 102:  
 103:     public Type SourceType { get { return typeof(Source); } }
 104:     public Type DestinationType { get { return typeof(Destination); } }
 105:  
 106:     protected Dictionary<string, string> Mappings { get; set; }
 107: }

There is quite a bit here but it's actually quite simple. When initialized, it calls the Setup function. This function sets up the default mappings. For example if you have two items that both have the Text field, it will set those up automatically. However in the constructor of the class that inherits from this class we can define the mapping to a greater degree. The first function to help with this is the Map function. This takes two Linq expressions. The advantage of doing this is that we can actually specify properties of properties. So for instance if we want the Length property of a string to be copied to an int property on the destination side, all we need to do is send in x=>x.Text.Length. The Map function will save the Text.Length portion of that so we can get to that value later on. The second function in there is Ignore. There might be times when you don't want those two Text fields to match up, so simply call ignore and it removes it from its list of mappings.

Past that there is only one more function of importance, Sync. Sync copies values from the source to the destination object. Since it already knows their types and what maps to what, it just goes down the list of mappings and copies them over. The two loops ensure that we get down to the individual properties if we're looking at nested properties. But that's all there is to it. Although it is a bit too basic at this step as it doesn't take into account type differences, string formatting, etc. But I'll talk about that next time. Anyway, take a look, leave feedback, and happy coding.

Oh and on an unrelated note, I find it sad that everyone I've talked to today has brought up the iPad in a positive light. First off I hate talking "technology" with people. Which is in quotes because they only bring up that stuff because they don't have much else in common with me as video games, cartoons, sci-fi stuff, bad B movies, XKCD, etc., which according to an article I read about why women don't go into tech related fields are "masculine" things (which I've never heard in conjunction with any of those items before reading that), are not common things that are enjoyed in my place of work. Not to mention their idea of tech talk is "it's cute/sexy"... Secondly I hate it being brought up because I think it's a dud of a product. No multitasking and lack of flash/Silverlight killed it for me. It means I can't stream AdultSwim or anywhere really, there was minimal talk about publisher partners (so iBook is a wait and see thing), I can't play games on Newgrounds or Kongregate (which have some games that are more addictive and better than anything I've seen on even the DS or PSP, let alone the iPhone). Plus the size is too big to really carry around easily. Plus you have to pay the monthly fee for the 3G (although no contract) and it's only AT&T which has terrible coverage in my area. Oh and since there is no multitasking so I can't even listen to Pandora while reading a book on it. Not to mention bringing the dock VGA connector with you just to do a presentation with it seems annoying to me (I always forget cords, etc.). So basically what I'm saying is I'm completely confused as to what niche it's suppose to fill in my life as it doesn't seem to do anything that I would want it to do.



Comments