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:

using System;
using System.Reflection;
using System.Collections;

namespace Binder
{
    public class ObjectBinder
    {
        ///
        /// Copies one object to an instance of a different type.
        ///
        /// Assumptions:
        /// Object which implements IList'1 also implements IList
        /// Object which implements IDictionary'2 also implements IDictionary
        /// 

        /// 
        ///The source.
        /// 
        public static T Copy(object source) where T : new()
        {
            return (T)Copy(source, typeof(T));
        }

        private static object Copy(object source, Type destinationType)
        {
            if (source == null) throw new ArgumentNullException("source", "source must not be null");

            var destination = Activator.CreateInstance(destinationType);

            var sourceProperties = source.GetType().GetProperties();
            foreach (var sourceProp in sourceProperties)
            {
                var sourcePropType = sourceProp.PropertyType;
                var destinationProp = destination.GetType().GetProperty(sourceProp.Name);

                if (sourceProp.CanRead && destinationProp != null && destinationProp.CanWrite)
                {
                    var destinationPropType = destinationProp.PropertyType;
                    Type sourcePropInterfaceType = null;
                    Type destinationPropInterfaceType = null;

                    // Skip indexer properties
                    if (sourceProp.GetIndexParameters().Length > 0) continue;

                    var sourceValue = sourceProp.GetValue(source, null);
                    if (sourceValue == null) continue;

                    object destinationValue = null;
                    if (destinationPropType.IsAssignableFrom(sourcePropType))
                    {
                        destinationValue = sourceValue;
                    }
                    else if (
                        (sourcePropInterfaceType = sourcePropType.GetInterface("IList`1", true)) != null &&
                        (destinationPropInterfaceType = destinationPropType.GetInterface("IList`1", true)) != null)
                    {

                        var sourceArgType = sourcePropInterfaceType.GetGenericArguments()[0];
                        var destArgType = destinationPropInterfaceType.GetGenericArguments()[0];
                        bool isAssignable = destArgType.IsAssignableFrom(sourceArgType);

                        IList listCopy = null;

                        if (destinationPropType.IsArray)
                        {
                            // If array there must be a one argument constructor
                            listCopy = (IList)Activator.CreateInstance(destinationPropType, ((IList)sourceValue).Count);
                        }
                        else
                        {
                            // If not an array require a parameterless constructor
                            if (destinationPropType.GetConstructor(Type.EmptyTypes) == null) continue;
                            listCopy = (IList)Activator.CreateInstance(destinationPropType);
                        }

                        int index = 0;
                        foreach (var item in ((IList)sourceValue))
                        {
                            var itemCopy = item;
                            if (!isAssignable)
                                itemCopy = Copy(item, destArgType);

                            if (destinationPropType.IsArray)
                                listCopy[index] = itemCopy;
                            else
                                listCopy.Add(itemCopy);

                            index++;
                        }

                        destinationValue = listCopy;
                    }
                    else if (
                        (sourcePropInterfaceType = sourcePropType.GetInterface("IDictionary`2", true)) != null &&
                        (destinationPropInterfaceType = destinationPropType.GetInterface("IDictionary`2", true)) != null)
                    {

                        var sourceArgTypes = sourcePropInterfaceType.GetGenericArguments();
                        var destArgTypes = destinationPropInterfaceType.GetGenericArguments();
                        bool isKeyAssignable = destArgTypes[0].IsAssignableFrom(sourceArgTypes[0]);
                        bool isValueAssignable = destArgTypes[1].IsAssignableFrom(sourceArgTypes[1]);

                        var defaultConstructor = destinationPropType.GetConstructor(Type.EmptyTypes);
                        if (defaultConstructor == null) continue;
                        var dictionaryCopy = Activator.CreateInstance(destinationPropType) as IDictionary;

                        foreach (DictionaryEntry pair in ((IDictionary)sourceValue))
                        {
                            var keyCopy = pair.Key;
                            var valueCopy = pair.Value;
                            if (!isKeyAssignable)
                                keyCopy = Copy(keyCopy, destArgTypes[0]);
                            if (!isValueAssignable)
                                valueCopy = Copy(valueCopy, destArgTypes[1]);

                            dictionaryCopy.Add(keyCopy, valueCopy);
                        }

                        destinationValue = dictionaryCopy;
                    }
                    else
                    {
                        destinationValue = Copy(sourceValue, destinationPropType);
                    }

                    destinationProp.SetValue(destination, destinationValue, null);

                }
            }

            return destination;
        }
    }
}

And the tests using XUnit.Net:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Xunit;
using System.Collections.ObjectModel;
 
namespace Binder
{
    public class ObjectBinderFacts
    {
        public class The_Copy_Method
        {
 
            [Fact]
            public void throws_if_source_is_null()
            {
 
                Exception ex = Record.Exception(() => ObjectBinder.Copy<Destination>(null));
 
                Assert.IsType<ArgumentNullException>(ex);
                Assert.Equal("source", ((ArgumentNullException)ex).ParamName);
            }
 
            [Fact]
            public void will_copy_assignable_scalar_property()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.A, res.A);
                Assert.Equal(source.B, res.B);
                Assert.Equal(source.C, res.C);
                Assert.Equal(source.D, res.D);
                Assert.Equal(source.E, res.E);
 
            }
 
            [Fact]
            public void will_copy_assignable_generic_list_property()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.F.Count, res.F.Count);
                Assert.Equal(source.F[0], res.F[0]);
                Assert.Equal(source.F[1], res.F[1]);
 
            }
 
            [Fact]
            public void will_copy_assignable_array_property()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.N.Length, res.N.Length);
                Assert.Equal(source.N[0], res.N[0]);
                Assert.Equal(source.N[1], res.N[1]);
 
            }
 
            [Fact]
            public void will_copy_unassignable_array_property()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.O.Length, res.O.Length);
                Assert.Equal(source.O[0].Value, res.O[0].Value);
                Assert.Equal(source.O[1].Value, res.O[1].Value);
 
            }
 
            [Fact]
            public void will_copy_assignable_generic_dictionary_property()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.J.Count, res.J.Count);
                Assert.Equal(source.J["a"], res.J["a"]);
                Assert.Equal(source.J["b"], res.J["b"]);
 
            }
 
            [Fact]
            public void will_copy_unassignable_generic_dictionary_with_assignable_keys_and_values()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.K.Count, res.K.Count);
                Assert.Equal(source.K["a"], res.K["a"]);
                Assert.Equal(source.K["b"], res.K["b"]);
 
            }
 
            [Fact]
            public void will_copy_unassignable_generic_dictionary_with_assignable_keys_and_unassignable_values()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.L.Count, res.L.Count);
                Assert.Equal(source.L["a"].Value, res.L["a"].Value);
                Assert.Equal(source.L["b"].Value, res.L["b"].Value);
 
            }
 
            [Fact]
            public void will_copy_unassignable_generic_dictionary_with_unassignable_keys_and_unassignable_values()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.M.Count, res.M.Count);
                Assert.Equal(source.M[new SomeObject { Value = 1 }].Value, res.M[new OtherObject { Value = 1 }].Value);
                Assert.Equal(source.M[new SomeObject { Value = 2 }].Value, res.M[new OtherObject { Value = 2 }].Value);
 
            }
 
            [Fact]
            public void will_copy_unassignable_generic_list_with_assignable_values()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.G.Count, res.G.Count);
                Assert.Equal(source.G[0], res.G[0]);
                Assert.Equal(source.G[1], res.G[1]);
 
            }
 
            [Fact]
            public void will_copy_unassignable_object_property()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.H.Value, res.H.Value);
 
            }
 
            [Fact]
            public void will_copy_unassignable_generic_list_properties()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<Destination>(source);
 
                Assert.Equal(source.I.Count, res.I.Count);
                Assert.Equal(source.I[0].Value, res.I[0].Value);
                Assert.Equal(source.I[1].Value, res.I[1].Value);
 
            }
 
            [Fact]
            public void will_return_object_with_default_values_if_no_properties_match()
            {
                var source = new Source();
 
                var res = ObjectBinder.Copy<SomeObject>(source);
 
                Assert.Equal(0, res.Value);
            }
 
            class SomeObject
            {
                public int Value { get; set; }
 
                public override int GetHashCode()
                {
                    return Value;
                }
 
                public override bool Equals(object obj)
                {
                    var some = obj as SomeObject;
                    if (obj == null) return false;
                    return some.Value == Value;
                }
            }
 
            class OtherObject
            {
                public int Value { get; set; }
 
                public override int GetHashCode()
                {
                    return Value;
                }
 
                public override bool Equals(object obj)
                {
                    var some = obj as OtherObject;
                    if (obj == null) return false;
                    return some.Value == Value;
                }
            }
 
            class OtherDictionary<TKey, TValue> : Dictionary<TKey, TValue>
            {
 
            }
 
            class Source
            {
                public Source()
                {
                    A = "String";
                    B = 5;
                    C = Guid.NewGuid();
                    D = 5.5;
                    E = 'a';
                    F = new List<int> { 1, 2 };
                    G = new List<string> { "a", "b" };
                    H = new SomeObject();
                    I = new List<SomeObject> { new SomeObject { Value = 1 }, new SomeObject { Value = 2 } };
                    J = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } };
                    K = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } };
                    L = new Dictionary<string, SomeObject> { { "a", new SomeObject { Value = 1 } }, { "b", new SomeObject { Value = 2 } } };
                    M = new Dictionary<SomeObject, SomeObject> {
                        { new SomeObject { Value = 1 }, new SomeObject { Value = 10 } }, 
                        { new SomeObject { Value = 2 }, new SomeObject { Value = 20 } } 
                    };
                    N = new[] { 1, 2 };
                    O = new[] { new SomeObject { Value = 1 }, new SomeObject { Value = 2 } };
                }
 
                // Assignable
                public string A { get; set; }
                public int B { get; set; }
                public Guid C { get; set; }
                public double D { get; set; }
                public char E { get; set; }
                public List<int> F { get; set; }
                public Dictionary<string, int> J { get; set; }
                public int[] N { get; set; }
 
                // Not assignable
                public List<string> G { get; set; }
                public SomeObject H { get; set; }
                public List<SomeObject> I { get; set; }
                public Dictionary<string, int> K { get; set; }
                public Dictionary<string, SomeObject> L { get; set; }
                public Dictionary<SomeObject, SomeObject> M { get; set; }
                public SomeObject[] O { get; set; }
            }
 
 
            class Destination
            {
                public string A { get; set; }
                public int B { get; set; }
                public Guid C { get; set; }
                public double D { get; set; }
                public char E { get; set; }
                public List<int> F { get; set; }
                public Dictionary<string, int> J { get; set; }
                public int[] N { get; set; }
 
                public Collection<string> G { get; set; }
                public OtherObject H { get; set; }
                public List<OtherObject> I { get; set; }
                public OtherDictionary<string, int> K { get; set; }
                public Dictionary<string, OtherObject> L { get; set; }
                public Dictionary<OtherObject, OtherObject> M { get; set; }
                public OtherObject[] O { get; set; }
            }
        }
    }
}

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.