Monday, 26 April 2010

.NET Deep Copy/Deep Clone between 2 different object types in C# – Some of the Different/Alternative Approaches

I’ve had to use this “Translation” pattern (and similar code) several times over the last few years – so I’ve decided to commit it to a blog entry for future reference.

If you have 2 objects (of different types e.g. say 2 different generated WCF client proxies pointing to SAP), with similar generated properties (e.g. both types have the same “Employee” structure with UserId, UserName, Display Name, then you could spend a lot of time trying to write code manually. You could end up writing mapping code like this:

employeeDto.EmployeeId = currentEmployee.EmployeeId
employeeDto.UserId = currentEmployee.UserId
employeeDto.Department = currentEmployee.Department
etc.

If you have a simple, flat object that has just value types (e.g. int, strings) with no complex objects (e.g. arrays, generic lists or reference objects) within, then you can simply use a “Shallow Copy” (also known as a “Memberwise Clone”) of the properties between the 2 objects.

For example:

/// 
/// Shallow Property Example
/// 
/// 
/// 
public static void CopyPropertyValues(object source, object destination)
{
    var destProperties = destination.GetType().GetProperties();
    foreach (var sourceProperty in source.GetType().GetProperties())
    {
        foreach (var destProperty in destProperties)
        {
            if (destProperty.Name == sourceProperty.Name &&
        destProperty.PropertyType.IsAssignableFrom(sourceProperty.PropertyType))
            {
                destProperty.SetValue(destination, sourceProperty.GetValue(
                    source, new object[] { }), new object[] { });

                break;
            }
        }
    }
}


If (as in most situations, your objects have child collections) e.g like a Purchase Order would have Purchase order items, then the Shallow copy approach will not work. If you have reference objects, it will actually just copy the references themselves – so the original object and the cloned object effectively have the same references and become the same object. As the references are the same, changes to the "cloned" object will be reflected in the original object - which is typically NOT what you want!

To solve this problem, you need to perform a Deep copy of your objects that will in fact traverse your object graph (ie full structure/hierarchy of your objects) to copy all properties and “subproperties” between the 2 different objects (of different types).

Deep Copy – for Same Object Type
There are approaches involving serialization and the IClonable interface (see http://en.csharp-online.net/ICloneable) for details – but they only work within the bounds of a single object type (ie they can’t map/translate the properties between 2 different types) – plus it only works with Serializable types:

(Sample taken from here http://www.codeproject.com/KB/tips/SerializedObjectCloner.aspx)

using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;     
      
/// 
/// Provides a method for performing a deep copy of an object.
/// Binary Serialization is used to perform the copy.
/// 
public static class ObjectCopier
{
 /// 
 /// Perform a deep Copy of the object.
 /// 
 /// The type of object being copied.
 /// 
The object instance to copy./// The copied object.
 public static T Clone(T source)
 {
  if (!typeof(T).IsSerializable)
  {
    throw new ArgumentException("The type must be serializable.", "source");
  }
  
  // Don't serialize a null object, simply return the default for that object
  if (Object.ReferenceEquals(source, null))
  {
    return default(T);
  }

  IFormatter formatter = new BinaryFormatter();
  Stream stream = new MemoryStream();
  using (stream)
  {
    formatter.Serialize(stream, source);
    stream.Seek(0, SeekOrigin.Begin);
    return (T)formatter.Deserialize(stream);
  }
 }
}

Another sample is here (also only works for deep copies between the same object types)
http://ox.no/posts/extension-methods-for-copying-or-cloning-objects

So for a deep copy approach for (which is what we need for most projects), you can do the following:
• Use Reflection to navigate through the object graph and copy each property one by one (rather than just copying the pointers/references as in the Shallow copy approach) This is a tried and trusted approach, but the performance (as it is using reflection) is not as good as the Expression approach.
• Use Reflection and Emit of IL Code/Expressions (only with .NET 3.5) to perform the copy - this performs better when there are a large number of runs as it runs compiled after the first run (after the initial reflection generates the code) - example can be found here - http://whizzodev.blogspot.com/2008/06/object-deep-cloning-using-il-in-c_20.html
• Use a 3rd Party utility library (which typically use the approaches above) – such as Automapper (http://automapper.codeplex.com/)

I’ve provided samples/details of the Reflection approach below:

Deep Copy Using Reflection, Supporting Different Object Types
(Sample from http://stackoverflow.com/questions/569154/how-to-deep-copy-between-objects-of-different-types-in-c-net)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace DDKOnline.Common
{
    /// 
    /// Utility 
    /// 
    public static class DeepCopyUtility
    {

        ///  
        /// Copies the data of one object to another. The target object gets properties of the first.  
        /// Any matching properties (by name) are written to the target. 
        ///  
        /// 
The source object to copy from/// 
The target object to copy topublic static void CopyObjectData(object source, object target)
        {
            CopyObjectData(source, target, String.Empty, BindingFlags.Public | BindingFlags.Instance);
        }

        ///  
        /// Copies the data of one object to another. The target object gets properties of the first.  
        /// Any matching properties (by name) are written to the target. 
        ///  
        /// 
The source object to copy from/// 
The target object to copy to/// 
A comma delimited list of properties that should not be copied/// 
Reflection binding accesspublic static void CopyObjectData(object source, object target, string excludedProperties, BindingFlags memberAccess)
        {
            string[] excluded = null;
            if (!string.IsNullOrEmpty(excludedProperties))
            {
                excluded = excludedProperties.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries);
            }

            MemberInfo[] miT = target.GetType().GetMembers(memberAccess);
            foreach (MemberInfo Field in miT)
            {
                string name = Field.Name;

                // Skip over excluded properties 
                if (string.IsNullOrEmpty(excludedProperties) == false
                    && excluded.Contains(name))
                {
                    continue;
                }


                if (Field.MemberType == MemberTypes.Field)
                {
                    FieldInfo sourcefield = source.GetType().GetField(name);
                    if (sourcefield == null) { continue; }

                    object SourceValue = sourcefield.GetValue(source);
                    ((FieldInfo)Field).SetValue(target, SourceValue);
                }
                else if (Field.MemberType == MemberTypes.Property)
                {
                    PropertyInfo piTarget = Field as PropertyInfo;
                    PropertyInfo sourceField = source.GetType().GetProperty(name, memberAccess);
                    if (sourceField == null) { continue; }

                    if (piTarget.CanWrite && sourceField.CanRead)
                    {
                        object targetValue = piTarget.GetValue(target, null);
                        object sourceValue = sourceField.GetValue(source, null);

                        if (sourceValue == null) { continue; }

                        if (sourceField.PropertyType.IsArray
                            && piTarget.PropertyType.IsArray
                            && sourceValue != null)
                        {
                            CopyArray(source, target, memberAccess, piTarget, sourceField, sourceValue);
                        }
                        else
                        {
                            CopySingleData(source, target, memberAccess, piTarget, sourceField, targetValue, sourceValue);
                        }
                    }
                }
            }
        }

        private static void CopySingleData(object source, object target, BindingFlags memberAccess, PropertyInfo piTarget, PropertyInfo sourceField, object targetValue, object sourceValue)
        {
            //instantiate target if needed 
            if (targetValue == null
                && piTarget.PropertyType.IsValueType == false
                && piTarget.PropertyType != typeof(string))
            {
                if (piTarget.PropertyType.IsArray)
                {
                    targetValue = Activator.CreateInstance(piTarget.PropertyType.GetElementType());
                }
                else
                {
                    targetValue = Activator.CreateInstance(piTarget.PropertyType);
                }
            }

            if (piTarget.PropertyType.IsValueType == false
                && piTarget.PropertyType != typeof(string))
            {
                CopyObjectData(sourceValue, targetValue, "", memberAccess);
                piTarget.SetValue(target, targetValue, null);
            }
            else
            {
                if (piTarget.PropertyType.FullName == sourceField.PropertyType.FullName)
                {
                    object tempSourceValue = sourceField.GetValue(source, null);
                    piTarget.SetValue(target, tempSourceValue, null);
                }
                else
                {
                    CopyObjectData(piTarget, target, "", memberAccess);
                }
            }
        }

        private static void CopyArray(object source, object target, BindingFlags memberAccess, PropertyInfo piTarget, PropertyInfo sourceField, object sourceValue)
        {
            int sourceLength = (int)sourceValue.GetType().InvokeMember("Length", BindingFlags.GetProperty, null, sourceValue, null);
            Array targetArray = Array.CreateInstance(piTarget.PropertyType.GetElementType(), sourceLength);
            Array array = (Array)sourceField.GetValue(source, null);

            for (int i = 0; i < array.Length; i++)
            {
                object o = array.GetValue(i);
                object tempTarget = Activator.CreateInstance(piTarget.PropertyType.GetElementType());
                CopyObjectData(o, tempTarget, "", memberAccess);
                targetArray.SetValue(tempTarget, i);
            }
            piTarget.SetValue(target, targetArray, null);
        } 

    }
}

6 comments:

Colby Africa said...

Thanks man!

Dagga0wnzU said...

Very Nice -- I started coding up the same exact thing, then I stumbled upon your piece -- saved me tons of time. Doesn't seem to handle Generic List type collections though. Once I update it to handl List I'll post it up for ya.

Thanks again bro!

Anu Garg said...

Thanks a lot for such a nice topic. It helped me & saved my time . I added code for List also as per my requirement. I hope same will help to others also.

private static void CopyList(object source, object target, BindingFlags memberAccess, PropertyInfo piTarget, PropertyInfo sourceField, object sourceValue)
{
int sourceLength = (int)sourceValue.GetType().GetProperty("Count").GetValue(sourceValue);
var listType = typeof(List<>);
Type[] genericArgs = sourceField.PropertyType.GetGenericArguments();
var concreteType = listType.MakeGenericType(genericArgs);
var newList = (IList)Activator.CreateInstance(concreteType);

var obj = (IList)sourceField.GetValue(source, null);
for (int i = 0; i < sourceLength; i++)
{
object[] ind = { i };
object o = obj[i];
object tempTarget = Activator.CreateInstance(piTarget.PropertyType.GetGenericArguments()[0]);
CopyObjectData(o, tempTarget, "", memberAccess);
newList.Insert(i, tempTarget);
}
piTarget.SetValue(target, newList, null);
}

Luke Afsar said...

Hi Anu,

That is great and it helped me a lot. One minor correction I needed to make for the CopyList function was the Type[] genericArgs = piTarget.PropertyType.GetGenericArguments() instead of using sourceField; since we are generating a new list of the target to set the values for.

Kavya Shetty said...
This comment has been removed by the author.
Kavya Shetty said...

Hi Anu,
How did you integrate the copy list code with CopyObjectData method. Can you please add that code as well.