Share via


Model Binder

When working with a multi-tier application I often find myself converting from one of the tiers object models to my own similar (but often different) model.  I often write code that would set one by one each property from a web tier object to my object.  In order to make this easier I wrote this object binder.  It is a fake deep copy in a way.  You tell it what destination type you want to build and give it a source object and it will go through each property (matching by names) and checks if it can copy from that property from the source  to the destination.  If it can’t it will call itself again on that object to try to copy its properties.

 

Here is the code:

    1: using System;
    2: using System.Reflection;
    3: using System.Collections;
    4:  
    5: namespace Binder
    6: {
    7:     public class ObjectBinder
    8:     {
    9:         /// <summary>
   10:         /// Copies one object to an instance of a different type.
   11:         /// 
   12:         /// Assumptions:
   13:         /// Object which implements IList'1 also implements IList 
   14:         /// Object which implements IDictionary'2 also implements IDictionary 
   15:         /// </summary>
   16:         /// <typeparam name="T"></typeparam>
   17:         /// <param name="source">The source.</param>
   18:         /// <returns></returns>
   19:         public static T Copy<T>(object source) where T : new()
   20:         {
   21:             return (T)Copy(source, typeof(T));
   22:         }
   23:  
   24:  
   25:         private static object Copy(object source, Type destinationType)
   26:         {
   27:             if (source == null) throw new ArgumentNullException("source", "source must not be null");
   28:  
   29:             var destination = Activator.CreateInstance(destinationType);
   30:  
   31:             var sourceProperties = source.GetType().GetProperties();
   32:             foreach (var sourceProp in sourceProperties)
   33:             {
   34:                 var sourcePropType = sourceProp.PropertyType;
   35:                 var destinationProp = destination.GetType().GetProperty(sourceProp.Name);
   36:  
   37:                 if (sourceProp.CanRead && destinationProp != null && destinationProp.CanWrite)
   38:                 {
   39:                     var destinationPropType = destinationProp.PropertyType;
   40:                     Type sourcePropInterfaceType = null;
   41:                     Type destinationPropInterfaceType = null;
   42:  
   43:                     // Skip indexer properties
   44:                     if (sourceProp.GetIndexParameters().Length > 0) continue;
   45:                     
   46:                     var sourceValue = sourceProp.GetValue(source, null);
   47:                     if (sourceValue == null) continue;
   48:  
   49:                     object destinationValue = null;
   50:                     if (destinationPropType.IsAssignableFrom(sourcePropType))
   51:                     {
   52:                         destinationValue = sourceValue;
   53:                     }
   54:                     else if (
   55:                         (sourcePropInterfaceType = sourcePropType.GetInterface("IList`1", true)) != null &&
   56:                         (destinationPropInterfaceType = destinationPropType.GetInterface("IList`1", true)) != null)
   57:                     {
   58:  
   59:                         var sourceArgType = sourcePropInterfaceType.GetGenericArguments()[0];
   60:                         var destArgType = destinationPropInterfaceType.GetGenericArguments()[0];
   61:                         bool isAssignable = destArgType.IsAssignableFrom(sourceArgType);
   62:  
   63:                         IList listCopy = null;
   64:  
   65:                         if (destinationPropType.IsArray)
   66:                         {
   67:                             // If array there must be a one argument constructor
   68:                             listCopy = (IList)Activator.CreateInstance(destinationPropType, ((IList)sourceValue).Count);
   69:                         }
   70:                         else
   71:                         {
   72:                             // If not an array require a parameterless constructor
   73:                             if (destinationPropType.GetConstructor(Type.EmptyTypes) == null) continue;
   74:                             listCopy = (IList)Activator.CreateInstance(destinationPropType);
   75:                         }
   76:  
   77:                         int index = 0;
   78:                         foreach (var item in ((IList)sourceValue))
   79:                         {
   80:                             var itemCopy = item;
   81:                             if (!isAssignable)
   82:                                 itemCopy = Copy(item, destArgType);
   83:  
   84:                             if (destinationPropType.IsArray)
   85:                                 listCopy[index] = itemCopy;
   86:                             else
   87:                                 listCopy.Add(itemCopy);
   88:  
   89:                             index++;
   90:                         }
   91:  
   92:                         destinationValue = listCopy;
   93:                     }
   94:                     else if (
   95:                         (sourcePropInterfaceType = sourcePropType.GetInterface("IDictionary`2", true)) != null &&
   96:                         (destinationPropInterfaceType = destinationPropType.GetInterface("IDictionary`2", true)) != null)
   97:                     {
   98:  
   99:                         var sourceArgTypes = sourcePropInterfaceType.GetGenericArguments();
  100:                         var destArgTypes = destinationPropInterfaceType.GetGenericArguments();
  101:                         bool isKeyAssignable = destArgTypes[0].IsAssignableFrom(sourceArgTypes[0]);
  102:                         bool isValueAssignable = destArgTypes[1].IsAssignableFrom(sourceArgTypes[1]);
  103:  
  104:                         var defaultConstructor = destinationPropType.GetConstructor(Type.EmptyTypes);
  105:                         if (defaultConstructor == null) continue;
  106:                         var dictionaryCopy = Activator.CreateInstance(destinationPropType) as IDictionary;
  107:  
  108:                         foreach (DictionaryEntry pair in ((IDictionary)sourceValue))
  109:                         {
  110:                             var keyCopy = pair.Key;
  111:                             var valueCopy = pair.Value;
  112:                             if (!isKeyAssignable)
  113:                                 keyCopy = Copy(keyCopy, destArgTypes[0]);
  114:                             if (!isValueAssignable)
  115:                                 valueCopy = Copy(valueCopy, destArgTypes[1]);
  116:  
  117:                             dictionaryCopy.Add(keyCopy, valueCopy);
  118:                         }
  119:  
  120:                         destinationValue = dictionaryCopy;
  121:                     }
  122:                     else
  123:                     {
  124:                         destinationValue = Copy(sourceValue, destinationPropType);
  125:                     }
  126:  
  127:  
  128:                     destinationProp.SetValue(destination, destinationValue, null);
  129:  
  130:                 }
  131:             }
  132:  
  133:  
  134:             return destination;
  135:         }
  136:     }
  137: }

And the tests using XUnit.Net:

    1: using System;
    2: using System.Collections.Generic;
    3: using System.Linq;
    4: using System.Text;
    5: using Xunit;
    6: using System.Collections.ObjectModel;
    7:  
    8: namespace Binder
    9: {
   10:     public class ObjectBinderFacts
   11:     {
   12:         public class The_Copy_Method
   13:         {
   14:  
   15:             [Fact]
   16:             public void throws_if_source_is_null()
   17:             {
   18:  
   19:                 Exception ex = Record.Exception(() => ObjectBinder.Copy<Destination>(null));
   20:  
   21:                 Assert.IsType<ArgumentNullException>(ex);
   22:                 Assert.Equal("source", ((ArgumentNullException)ex).ParamName);
   23:             }
   24:  
   25:             [Fact]
   26:             public void will_copy_assignable_scalar_property()
   27:             {
   28:                 var source = new Source();
   29:  
   30:                 var res = ObjectBinder.Copy<Destination>(source);
   31:  
   32:                 Assert.Equal(source.A, res.A);
   33:                 Assert.Equal(source.B, res.B);
   34:                 Assert.Equal(source.C, res.C);
   35:                 Assert.Equal(source.D, res.D);
   36:                 Assert.Equal(source.E, res.E);
   37:  
   38:             }
   39:  
   40:             [Fact]
   41:             public void will_copy_assignable_generic_list_property()
   42:             {
   43:                 var source = new Source();
   44:  
   45:                 var res = ObjectBinder.Copy<Destination>(source);
   46:  
   47:                 Assert.Equal(source.F.Count, res.F.Count);
   48:                 Assert.Equal(source.F[0], res.F[0]);
   49:                 Assert.Equal(source.F[1], res.F[1]);
   50:  
   51:             }
   52:  
   53:             [Fact]
   54:             public void will_copy_assignable_array_property()
   55:             {
   56:                 var source = new Source();
   57:  
   58:                 var res = ObjectBinder.Copy<Destination>(source);
   59:  
   60:                 Assert.Equal(source.N.Length, res.N.Length);
   61:                 Assert.Equal(source.N[0], res.N[0]);
   62:                 Assert.Equal(source.N[1], res.N[1]);
   63:  
   64:             }
   65:  
   66:             [Fact]
   67:             public void will_copy_unassignable_array_property()
   68:             {
   69:                 var source = new Source();
   70:  
   71:                 var res = ObjectBinder.Copy<Destination>(source);
   72:  
   73:                 Assert.Equal(source.O.Length, res.O.Length);
   74:                 Assert.Equal(source.O[0].Value, res.O[0].Value);
   75:                 Assert.Equal(source.O[1].Value, res.O[1].Value);
   76:  
   77:             }
   78:  
   79:             [Fact]
   80:             public void will_copy_assignable_generic_dictionary_property()
   81:             {
   82:                 var source = new Source();
   83:  
   84:                 var res = ObjectBinder.Copy<Destination>(source);
   85:  
   86:                 Assert.Equal(source.J.Count, res.J.Count);
   87:                 Assert.Equal(source.J["a"], res.J["a"]);
   88:                 Assert.Equal(source.J["b"], res.J["b"]);
   89:  
   90:             }
   91:  
   92:             [Fact]
   93:             public void will_copy_unassignable_generic_dictionary_with_assignable_keys_and_values()
   94:             {
   95:                 var source = new Source();
   96:  
   97:                 var res = ObjectBinder.Copy<Destination>(source);
   98:  
   99:                 Assert.Equal(source.K.Count, res.K.Count);
  100:                 Assert.Equal(source.K["a"], res.K["a"]);
  101:                 Assert.Equal(source.K["b"], res.K["b"]);
  102:  
  103:             }
  104:  
  105:             [Fact]
  106:             public void will_copy_unassignable_generic_dictionary_with_assignable_keys_and_unassignable_values()
  107:             {
  108:                 var source = new Source();
  109:  
  110:                 var res = ObjectBinder.Copy<Destination>(source);
  111:  
  112:                 Assert.Equal(source.L.Count, res.L.Count);
  113:                 Assert.Equal(source.L["a"].Value, res.L["a"].Value);
  114:                 Assert.Equal(source.L["b"].Value, res.L["b"].Value);
  115:  
  116:             }
  117:  
  118:             [Fact]
  119:             public void will_copy_unassignable_generic_dictionary_with_unassignable_keys_and_unassignable_values()
  120:             {
  121:                 var source = new Source();
  122:  
  123:                 var res = ObjectBinder.Copy<Destination>(source);
  124:  
  125:                 Assert.Equal(source.M.Count, res.M.Count);
  126:                 Assert.Equal(source.M[new SomeObject { Value = 1 }].Value, res.M[new OtherObject { Value = 1 }].Value);
  127:                 Assert.Equal(source.M[new SomeObject { Value = 2 }].Value, res.M[new OtherObject { Value = 2 }].Value);
  128:  
  129:             }
  130:  
  131:             [Fact]
  132:             public void will_copy_unassignable_generic_list_with_assignable_values()
  133:             {
  134:                 var source = new Source();
  135:  
  136:                 var res = ObjectBinder.Copy<Destination>(source);
  137:  
  138:                 Assert.Equal(source.G.Count, res.G.Count);
  139:                 Assert.Equal(source.G[0], res.G[0]);
  140:                 Assert.Equal(source.G[1], res.G[1]);
  141:  
  142:             }
  143:  
  144:             [Fact]
  145:             public void will_copy_unassignable_object_property()
  146:             {
  147:                 var source = new Source();
  148:  
  149:                 var res = ObjectBinder.Copy<Destination>(source);
  150:  
  151:                 Assert.Equal(source.H.Value, res.H.Value);
  152:  
  153:             }
  154:  
  155:             [Fact]
  156:             public void will_copy_unassignable_generic_list_properties()
  157:             {
  158:                 var source = new Source();
  159:  
  160:                 var res = ObjectBinder.Copy<Destination>(source);
  161:  
  162:                 Assert.Equal(source.I.Count, res.I.Count);
  163:                 Assert.Equal(source.I[0].Value, res.I[0].Value);
  164:                 Assert.Equal(source.I[1].Value, res.I[1].Value);
  165:  
  166:             }
  167:  
  168:             [Fact]
  169:             public void will_return_object_with_default_values_if_no_properties_match()
  170:             {
  171:                 var source = new Source();
  172:  
  173:                 var res = ObjectBinder.Copy<SomeObject>(source);
  174:  
  175:                 Assert.Equal(0, res.Value);
  176:             }
  177:  
  178:             class SomeObject
  179:             {
  180:                 public int Value { get; set; }
  181:  
  182:                 public override int GetHashCode()
  183:                 {
  184:                     return Value;
  185:                 }
  186:  
  187:                 public override bool Equals(object obj)
  188:                 {
  189:                     var some = obj as SomeObject;
  190:                     if (obj == null) return false;
  191:                     return some.Value == Value;
  192:                 }
  193:             }
  194:  
  195:             class OtherObject
  196:             {
  197:                 public int Value { get; set; }
  198:  
  199:                 public override int GetHashCode()
  200:                 {
  201:                     return Value;
  202:                 }
  203:  
  204:                 public override bool Equals(object obj)
  205:                 {
  206:                     var some = obj as OtherObject;
  207:                     if (obj == null) return false;
  208:                     return some.Value == Value;
  209:                 }
  210:             }
  211:  
  212:             class OtherDictionary<TKey, TValue> : Dictionary<TKey, TValue>
  213:             {
  214:  
  215:             }
  216:  
  217:             class Source
  218:             {
  219:                 public Source()
  220:                 {
  221:                     A = "String";
  222:                     B = 5;
  223:                     C = Guid.NewGuid();
  224:                     D = 5.5;
  225:                     E = 'a';
  226:                     F = new List<int> { 1, 2 };
  227:                     G = new List<string> { "a", "b" };
  228:                     H = new SomeObject();
  229:                     I = new List<SomeObject> { new SomeObject { Value = 1 }, new SomeObject { Value = 2 } };
  230:                     J = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } };
  231:                     K = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } };
  232:                     L = new Dictionary<string, SomeObject> { { "a", new SomeObject { Value = 1 } }, { "b", new SomeObject { Value = 2 } } };
  233:                     M = new Dictionary<SomeObject, SomeObject> {
  234:                         { new SomeObject { Value = 1 }, new SomeObject { Value = 10 } }, 
  235:                         { new SomeObject { Value = 2 }, new SomeObject { Value = 20 } } 
  236:                     };
  237:                     N = new[] { 1, 2 };
  238:                     O = new[] { new SomeObject { Value = 1 }, new SomeObject { Value = 2 } };
  239:                 }
  240:  
  241:                 // Assignable
  242:                 public string A { get; set; }
  243:                 public int B { get; set; }
  244:                 public Guid C { get; set; }
  245:                 public double D { get; set; }
  246:                 public char E { get; set; }
  247:                 public List<int> F { get; set; }
  248:                 public Dictionary<string, int> J { get; set; }
  249:                 public int[] N { get; set; }
  250:  
  251:                 // Not assignable
  252:                 public List<string> G { get; set; }
  253:                 public SomeObject H { get; set; }
  254:                 public List<SomeObject> I { get; set; }
  255:                 public Dictionary<string, int> K { get; set; }
  256:                 public Dictionary<string, SomeObject> L { get; set; }
  257:                 public Dictionary<SomeObject, SomeObject> M { get; set; }
  258:                 public SomeObject[] O { get; set; }
  259:             }
  260:  
  261:  
  262:             class Destination
  263:             {
  264:                 public string A { get; set; }
  265:                 public int B { get; set; }
  266:                 public Guid C { get; set; }
  267:                 public double D { get; set; }
  268:                 public char E { get; set; }
  269:                 public List<int> F { get; set; }
  270:                 public Dictionary<string, int> J { get; set; }
  271:                 public int[] N { get; set; }
  272:  
  273:                 public Collection<string> G { get; set; }
  274:                 public OtherObject H { get; set; }
  275:                 public List<OtherObject> I { get; set; }
  276:                 public OtherDictionary<string, int> K { get; set; }
  277:                 public Dictionary<string, OtherObject> L { get; set; }
  278:                 public Dictionary<OtherObject, OtherObject> M { get; set; }
  279:                 public OtherObject[] O { get; set; }
  280:             }
  281:         }
  282:     }
  283: }

 

One possible enhancement I thought about was to add a attribute to indicate on the destination model that is maps to a property of a different name.  I haven’t had a need for it yet but I will surely add it when I do.