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.
source to share
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;
}
source to share