Shared deep cloning with limited depth in the tree hierarchy

I am using Code First development with Entity Framework. Problem in: when cloning a lazy loaded list, the list items are of type:

System.Data.Entity.DynamicProxies.Node_CB2936E7A8389F56009639CD3D732E4B509C4467531A6AFB3A143429D77A07DF

and my general function is seeing it as System.Object

. Is there a way to pass this object to the parent class before going to the function Clone

? Or other ideas?

Since I only need to clone up to a certain depth, I cannot serialize the entire tree structure and then deserialize it.

My model:

public class Node
{
    public int Id { get; set; }

    public String Name { get; set; } 

    public virtual IList<Node> Nodes { get; set; } 

    public int? ParentId { get; set; }

    [ForeignKey("ParentId")]
    public virtual Node Parent { get; set; }
}

      

And using the function to clone:

protected T Clone<T>(T entity, int depth) where T : new()
{
    var cloned = new T();
    foreach (var property in cloned.GetType().GetProperties())
    {
        if (property.PropertyType.Namespace == "System" && property.CanWrite)
        {
            property.SetValue(cloned, property.GetValue(entity));
        }
        else if (depth > 0 && property.CanWrite)
        {
            if (property.PropertyType.Namespace == "System.Collections.Generic")
            {
                var type = property.PropertyType.GetGenericArguments()[0];
                Type genericListType = typeof(List<>).MakeGenericType(type);
                var collection = (IList)Activator.CreateInstance(genericListType);
                var value = property.GetValue(entity);
                foreach (var element in value as IEnumerable)
                {
                    collection.Add(Clone(element, depth - 1));  // here is Error:
                        //The value "System.Object" is not of type "Sandbox.Models.Node" and cannot be used in this generic collection. Parameter name: value
                        //I should cast element to its parent class but how?
                }
                property.SetValue(cloned, collection);
            }
        }
    }
    return cloned;
}

      

This feature works fine on non-Entity Framework entities. And using the function Clone

:

var cloned = Clone(context.Nodes.Find(10), 2);

      

Any help would be appreciated.

+3


source to share


1 answer


The task you have var

in your foreach loop will be of type Object

, so new T();

when you call Clone

it internally, it will execute new Object()

instead of new WhateverTheTypeTheListHad()

. What you need to do is make this new call using reflection.

protected T Clone<T>(T entity, int depth) where T : new()
{
    return (T)CloneInternal(entity, depth);
}

private object CloneInternal(object entity, int depth)
{
    var cloned = Activator.CreateInstance(entity.GetType());

    foreach (var property in cloned.GetType().GetProperties())
    {
        if (property.PropertyType.Namespace == "System" && property.CanWrite)
        {
            property.SetValue(cloned, property.GetValue(entity));
        }
        else if (depth > 0 && property.CanWrite)
        {
            if (property.PropertyType.Namespace == "System.Collections.Generic")
            {
                var type = property.PropertyType.GetGenericArguments()[0];
                Type genericListType = typeof(List<>).MakeGenericType(type);
                var collection = (IList)Activator.CreateInstance(genericListType);
                var value = property.GetValue(entity);
                foreach (var element in value as IEnumerable)
                {
                    collection.Add(CloneInternal(element, depth - 1));
                }
                property.SetValue(cloned, collection);
            }
        }
    }
    return cloned;
}

      

Since recursive calls do not depend on knowledge of the passed type, I prefer to separate the logic and make the recursive inner version not generic and just pass the object. This makes it more obvious if you have false assumptions (like a call new

to Object

).



However, your code has other problems. For example, you do internal recursion for whatever type is in System.Collections.Generic

, but you always create List<genericListType>

if the collection was something else (such HashSet<genericListType>

as very common in EF) your code will not work per property.SetValue(cloned, collection);

call.

Below is a quick refactoring to handle any IList

, IList<T>

and ISet<T>

(and default constructor) related operations, which should cover 90% of all the collections you come across.

    private object CloneInternal(object entity, int depth)
    {
        var cloned = Activator.CreateInstance(entity.GetType());

        foreach (var property in cloned.GetType().GetProperties())
        {
            Type propertyType = property.PropertyType;
            if (propertyType.Namespace == "System" && property.CanWrite)
            {
                property.SetValue(cloned, property.GetValue(entity));
            }
            else if (depth > 0 && property.CanWrite && typeof(IEnumerable).IsAssignableFrom(propertyType))
            {
                if (typeof(IList).IsAssignableFrom(propertyType))
                {
                    var collection = (IList)Activator.CreateInstance(propertyType);
                    var value = property.GetValue(entity);
                    foreach (var element in value as IEnumerable)
                    {
                        collection.Add(CloneInternal(element, depth - 1));
                    }
                    property.SetValue(cloned, collection);
                }
                else if (propertyType.IsGenericType)
                {
                    var type = propertyType.GetGenericArguments().Single();
                    if (typeof(IList<>).MakeGenericType(type).IsAssignableFrom(propertyType) ||
                        typeof(ISet<>).MakeGenericType(type).IsAssignableFrom(propertyType))
                    {
                        var collection = Activator.CreateInstance(propertyType);
                        var addMethod = collection.GetType().GetMethod("Add");
                        var value = property.GetValue(entity);
                        foreach (var element in value as IEnumerable)
                        {
                            addMethod.Invoke(collection, new[] {CloneInternal(element, depth - 1)});
                        }
                        property.SetValue(cloned, collection);
                    }
                }
            }
        }
        return cloned;
    }

      

+1


source







All Articles