Revisiting Deep Copy in C#

In A different take on Deep Copy I talked a bit about the different approaches to how you can accomplish performing a Deep Copy of a model in C#. In this article I want to revisit the topic briefly and demonstrate two methods to handle this concern.

Q: Why not use Object.MemberwiseClone?
A: You can, but it only performs a shallow copy of your model. If you have a more complex model, you will need something that can dig deeper and map all your properties.

The first approach is our Deep Copy method done with serialization. If you have been around the Internet a bit, you will recognize it:


/// <summary>
/// Performs a Deep Copy of an object through serialization.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="oSource">The o source.</param>
/// <returns></returns>
public static T DeepCopy<T>(this T oSource)
{
    using (var ms = new MemoryStream())
    {
        var formatter = new BinaryFormatter();
        formatter.Serialize(ms, oSource);
        ms.Position = 0;
        return (T)formatter.Deserialize(ms);
    }
}

This is the cleanest way to copy a model.

1) A MemoryStream is created.
2) A BinaryFormatter serializes the source into the MemoryStream.
3) The MemoryStream position is reset for reading purposes.
4) A deserialized object is cast to type T and returned.

A simple example:


var myNewObject = myObject.DeepCopy();

 
This works great in many cases but not in a Framework like Silverlight. Given, not a lot of us use that anymore, but you could still run into a scenario where serialization is not possible. In that case, we are going to create a new instance of the model we intend to copy. Then, we will map properties between the 2 models.


/// <summary>
/// Copies all public properties from one class to another.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="destination">The destination.</param>
/// <exception cref="System.Exception">Source and destination cannot be null and must be 
/// of the same type!</exception>
public static void MapProperties<T>(this T source, T destination) where T : class
{
    // Source and destination must exist.
    if ((source == null || destination == null)) return;

    // Get properties
    var propertyInfos = source.GetType().GetProperties();
    if (!propertyInfos.Any()) return;

    // Process only public properties
    foreach (var propInfo in propertyInfos.Where(x => x.CanWrite))
    {
        // Get value from source and assign to destination.
        var value = propInfo.GetValue(source, null);
        if (value == null) continue;

        // Evaluate
        var valType = value.GetType();

        // Collections
        if (valType.InheritsFrom<ICollection>())
        {
            var sourceCollection = value as IList;
            if (sourceCollection == null) continue;

            // Create new instance of collection
            IList destinationCollection = null;
            destinationCollection = (valType.BaseType == typeof(Array))
                ? Array.CreateInstance(valType.GetElementType(), sourceCollection.Count)
                : (IList)Activator.CreateInstance(valType, null);
            if (destinationCollection == null) continue;

            // Map properties
            foreach (var item in sourceCollection)
            {
                // New item instance
                var newItem = HasDefaultConstructor(item.GetType()) 
                    ? Activator.CreateInstance(item.GetType(), null) 
                    : item;

                // Map properties
                item.MapProperties(newItem);

                // Add to destination collection
                if (valType.BaseType == typeof(Array))
                {
                    destinationCollection[sourceCollection.IndexOf(item)] = newItem;
                }
                else
                {
                    destinationCollection.Add(newItem);
                }
            }

            // Add new collection to destination
            propInfo.SetValue(destination, destinationCollection, null);
        }
        else
        {
            propInfo.SetValue(destination, value, null);
        }

        // Check for properties and propagate if they exist.
        var newPropInfos = value.GetType().GetProperties();
        if (!newPropInfos.Any()) continue;

        // Copy properties for each child where necessary.
        var childSource = source.GetType().GetProperty(propInfo.Name);
        var childDestination = destination.GetType().GetProperty(propInfo.Name);
        childSource.MapProperties(childDestination);
    }
}

/// <summary>
/// Determines whether the type has a default contructor.
/// </summary>
/// <param name="type">The type.</param>
/// <returns></returns>
private static bool HasDefaultConstructor(Type type)
{
    return
        type.GetConstructor(Type.EmptyTypes) != null ||
        type.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
            .Any(x => x.GetParameters().All(p => p.IsOptional));
}

Here is what is going on:

1) We get the properties associated with the class type.
2) We evaluate the public properties.
3) Using reflection, we retrieve the value from the source and apply it to the destination.
4) If the value is not null, we drill down further for more public properties recursively.
Note: Collections need a bit of extra work to ensure we don’t simply copy over their object instances.
5) Repeat until the entire model has been traversed.

A simple example:


SomeType myNewObject = new SomeType();

// Assumption: "myObject" is of type "SomeType"
myObject.MapProperties(myNewObject);

In conclusion: if you can’t serialize, map the properties between the 2 models.

That’s all for now.

 
Comments

No comments yet.

Leave a Reply