diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index 7523555c24..c1b45f63ce 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -9,10 +9,30 @@ namespace Umbraco.Core.Models { public static class DeepCloneHelper { + /// + /// Stores the metadata for the properties for a given type so we know how to create them + /// + private struct ClonePropertyInfo + { + public ClonePropertyInfo(PropertyInfo propertyInfo) : this() + { + if (propertyInfo == null) throw new ArgumentNullException("propertyInfo"); + PropertyInfo = propertyInfo; + } + + public PropertyInfo PropertyInfo { get; private set; } + public bool IsDeepCloneable { get; set; } + public Type GenericListType { get; set; } + public bool IsList + { + get { return GenericListType != null; } + } + } + /// /// Used to avoid constant reflection (perf) /// - private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); /// /// Used to deep clone any reference properties on the object (should be done after a MemberwiseClone for which the outcome is 'output') @@ -30,81 +50,99 @@ namespace Umbraco.Core.Models throw new InvalidOperationException("Both the input and output types must be the same"); } + //get the property metadata from cache so we only have to figure this out once per type var refProperties = PropCache.GetOrAdd(inputType, type => inputType.GetProperties() - .Where(x => - //is not attributed with the ignore clone attribute - x.GetCustomAttribute() == null + .Select(propertyInfo => + { + if ( + //is not attributed with the ignore clone attribute + propertyInfo.GetCustomAttribute() != null //reference type but not string - && x.PropertyType.IsValueType == false && x.PropertyType != typeof (string) + || propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof (string) //settable - && x.CanWrite + || propertyInfo.CanWrite == false //non-indexed - && x.GetIndexParameters().Any() == false) + || propertyInfo.GetIndexParameters().Any()) + { + return null; + } + + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + { + return new ClonePropertyInfo(propertyInfo) { IsDeepCloneable = true }; + } + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) + && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + { + if (propertyInfo.PropertyType.IsGenericType + && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))) + { + //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> + var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); + return new ClonePropertyInfo(propertyInfo) { GenericListType = genericType }; + } + if (propertyInfo.PropertyType.IsArray + || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) + { + //if its an array, we'll create a list to work with first and then convert to array later + //otherwise if its just a regular derivitave of IEnumerable, we can use a list too + return new ClonePropertyInfo(propertyInfo) { GenericListType = typeof(List) }; + } + //skip instead of trying to create instance of abstract or interface + if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) + { + return null; + } + + //its a custom IEnumerable, we'll try to create it + try + { + var custom = Activator.CreateInstance(propertyInfo.PropertyType); + //if it's an IList we can work with it, otherwise we cannot + var newList = custom as IList; + if (newList == null) + { + return null; + } + return new ClonePropertyInfo(propertyInfo) {GenericListType = propertyInfo.PropertyType}; + } + catch (Exception) + { + //could not create this type so we'll skip it + return null; + } + } + return new ClonePropertyInfo(propertyInfo); + }) + .Where(x => x.HasValue) + .Select(x => x.Value) .ToArray()); - foreach (var propertyInfo in refProperties) + foreach (var clonePropertyInfo in refProperties) { - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + if (clonePropertyInfo.IsDeepCloneable) { //this ref property is also deep cloneable so clone it - var result = (IDeepCloneable)propertyInfo.GetValue(input, null); + var result = (IDeepCloneable)clonePropertyInfo.PropertyInfo.GetValue(input, null); if (result != null) { //set the cloned value to the property - propertyInfo.SetValue(output, result.DeepClone(), null); + clonePropertyInfo.PropertyInfo.SetValue(output, result.DeepClone(), null); } } - else if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) - && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + else if (clonePropertyInfo.IsList) { - IList newList; - if (propertyInfo.PropertyType.IsGenericType - && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))) - { - //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> - var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); - newList = (IList)Activator.CreateInstance(genericType); - } - else if (propertyInfo.PropertyType.IsArray - || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) - { - //if its an array, we'll create a list to work with first and then convert to array later - //otherwise if its just a regular derivitave of IEnumerable, we can use a list too - newList = new List(); - } - else - { - //skip instead of trying to create instance of abstract or interface - if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) - { - continue; - } - - //its a custom IEnumerable, we'll try to create it - try - { - var custom = Activator.CreateInstance(propertyInfo.PropertyType); - //if it's an IList we can work with it, otherwise we cannot - newList = custom as IList; - if (newList == null) - { - continue; - } - } - catch (Exception) - { - //could not create this type so we'll skip it - continue; - } - } - - var enumerable = (IEnumerable)propertyInfo.GetValue(input, null); + var enumerable = (IEnumerable)clonePropertyInfo.PropertyInfo.GetValue(input, null); if (enumerable == null) continue; + var newList = (IList)Activator.CreateInstance(clonePropertyInfo.GenericListType); + var isUsableType = true; //now clone each item @@ -136,21 +174,21 @@ namespace Umbraco.Core.Models continue; } - if (propertyInfo.PropertyType.IsArray) + if (clonePropertyInfo.PropertyInfo.PropertyType.IsArray) { //need to convert to array - var arr = (object[])Activator.CreateInstance(propertyInfo.PropertyType, newList.Count); + var arr = (object[])Activator.CreateInstance(clonePropertyInfo.PropertyInfo.PropertyType, newList.Count); for (int i = 0; i < newList.Count; i++) { arr[i] = newList[i]; } //set the cloned collection - propertyInfo.SetValue(output, arr, null); + clonePropertyInfo.PropertyInfo.SetValue(output, arr, null); } else { //set the cloned collection - propertyInfo.SetValue(output, newList, null); + clonePropertyInfo.PropertyInfo.SetValue(output, newList, null); } }