/// <summary>
 /// (Recursive) Recursive function that inspects an object and its properties/fields and clones it
 /// </summary>
 /// <param name="sourceObject">The object to clone</param>
 /// <param name="currentDepth">The current tree depth</param>
 /// <param name="maxDepth">The max tree depth</param>
 /// <param name="configuration">The cloning options</param>
 /// <param name="objectTree">The object tree to prevent cyclical references</param>
 /// <param name="path">The current path being traversed</param>
 /// <returns></returns>
 private object InspectAndCopy(object sourceObject, int currentDepth, int maxDepth, CloneConfiguration configuration, ObjectTreeReferenceTracker objectTree, string path)
 => InspectAndCopy(sourceObject, null, null, currentDepth, maxDepth, configuration, objectTree, path, null);
Exemplo n.º 2
0
        /// <summary>
        /// Recursive function that inspects an object and its properties/fields and clones it to a new or existing type
        /// </summary>
        /// <param name="sourceObject">The object to clone</param>
        /// <param name="destinationObject">An existing object to clone values to</param>
        /// <param name="destinationObjectType">The type of the destination object to clone to</param>
        /// <param name="currentDepth">The current tree depth</param>
        /// <param name="maxDepth">The max tree depth</param>
        /// <param name="configuration">Configure custom cloning options</param>
        /// <param name="objectTree">The object tree to prevent cyclical references</param>
        /// <param name="path">The current path being traversed</param>
        /// <param name="ignorePropertiesOrPaths">A list of properties or paths to ignore</param>
        /// <returns></returns>
        private object InspectAndCopy(object sourceObject, object destinationObject, Type destinationObjectType, int currentDepth, int maxDepth, CloneConfiguration configuration, ObjectTreeReferenceTracker objectTree, string path, ICollection <string> ignorePropertiesOrPaths)
        {
            if (_hasIgnoreConfiguration == null)
            {
                _hasIgnoreConfiguration = HasIgnoreConfiguration(configuration, ignorePropertiesOrPaths);
            }

            if (_hasIgnoreConfiguration.Value && IgnoreObjectName(null, path, configuration, ignorePropertiesOrPaths))
            {
                return(null);
            }

            if (sourceObject == null)
            {
                return(null);
            }

            // ensure we don't go too deep if specified
            if (maxDepth > 0 && currentDepth >= maxDepth)
            {
                throw new CloneException($"The maximum clone recursion depth has exceeded maxDepth of '{maxDepth}'. Try setting the configuration option {nameof(CloneConfiguration.AllowUseCustomHashCodes)} to true or increase the {nameof(CloneConfiguration.MaxDepth)}. The last path traversed was '{path}'.", path);
            }

            var          type = sourceObject.GetType();
            ExtendedType typeSupport;

            try
            {
                typeSupport = type.GetExtendedType(DefaultExtendedTypeOptions);
            }
            // certain attributes such as .net remoting SoapTypeAttribute can cause issues trying to inspect. Possibly a framework bug with reflection
            catch (CustomAttributeFormatException)
            {
                return(sourceObject);
            }

            ExtendedType destinationTypeSupport;

            if (destinationObjectType == null || destinationObjectType == type)
            {
                destinationTypeSupport = typeSupport;
            }
            else
            {
                destinationTypeSupport = destinationObjectType.GetExtendedType(DefaultExtendedTypeOptions);
            }

            // always return the original value on value types
            if (typeSupport.IsValueType)
            {
                return(sourceObject);
            }

            // drop any objects we are ignoring by attribute
            if (_hasIgnoreConfiguration.Value && typeSupport.Attributes.Any(x => configuration.IgnorePropertiesWithAttributes.Contains(x.Name)))
            {
                return(null);
            }

            // for delegate types, copy them by reference rather than returning null
            if (typeSupport.IsDelegate)
            {
                return(sourceObject);
            }

            object newObject = null;

            // create a new empty object of the desired type
            if (typeSupport.IsArray)
            {
                if (!(sourceObject is Array sourceArray))
                {
                    throw new NullReferenceException($"{nameof(sourceArray)} cannot be null!");
                }
                // calculate the dimensions of the array
                var arrayRank = sourceArray.Rank;
                // get the length of each dimension
                var arrayDimensions = new List <int>();
                for (var dimension = 0; dimension < arrayRank; dimension++)
                {
                    arrayDimensions.Add(sourceArray.GetLength(dimension));
                }
                newObject = _objectFactory.CreateEmptyObject(destinationTypeSupport, default(TypeRegistry), arrayDimensions.ToArray());
            }
            else if (typeSupport.Type == typeof(string))
            {
                // copy the item directly
                newObject = string.Copy((string)sourceObject);
                return(newObject);
            }
            else
            {
                newObject = destinationObject ?? _objectFactory.CreateEmptyObject(destinationTypeSupport);
            }

            if (newObject == null)
            {
                return(null);
            }

            // increment the current recursion depth
            currentDepth++;

            // construct a hashtable of objects we have already inspected (recursion loop preventer)
            if (!typeSupport.IsValueType)
            {
                if (!objectTree.Contains(sourceObject))
                {
                    objectTree.Add(sourceObject);
                }
                else
                {
                    // object has already been traversed
                    return(sourceObject);
                }
            }

            // clone using IClonable interface if exists
            if (configuration.AllowIClonableImplementations && typeSupport.Interfaces.Contains(typeof(ICloneable)))
            {
                var iClonable = sourceObject as ICloneable;
                return(iClonable?.Clone());
            }

            // clone a dictionary's key/values
            else if (typeSupport.IsDictionary && typeSupport.IsGeneric)
            {
                var    genericType = typeSupport.Type.GetGenericArguments().ToList();
                Type[] typeArgs    = { genericType[0], genericType[1] };

                var listType      = typeof(Dictionary <,>).MakeGenericType(typeArgs);
                var newDictionary = Activator.CreateInstance(listType) as IDictionary;
                newObject = newDictionary ?? throw new NullReferenceException($"{nameof(newDictionary)} cannot be null");
                var iDictionary = sourceObject as IDictionary;
                var success     = false;
                var retryCount  = 0;
                while (!success && retryCount < 10)
                {
                    try
                    {
                        foreach (DictionaryEntry item in iDictionary)
                        {
                            var key   = InspectAndCopy(item.Key, null, null, currentDepth, maxDepth, configuration, objectTree, path, ignorePropertiesOrPaths);
                            var value = InspectAndCopy(item.Value, null, null, currentDepth, maxDepth, configuration, objectTree, path, ignorePropertiesOrPaths);
                            newDictionary.Add(key, value);
                        }
                        success = true;
                    }
                    catch (InvalidOperationException)
                    {
                        // if the collection was modified during enumeration, stop re-initialize and retry
                        success = false;
                        retryCount++;
                        newDictionary.Clear();
                    }
                }
                if (!success)
                {
                    throw new CloneException($"Error cloning Dictionary<,> at path '{path}'. Ensure the object is not modified while cloning data utilizing thread-safe access.");
                }
                return(newObject);
            }
            else if (typeSupport.IsHashtable && !typeSupport.IsGeneric)
            {
                var newHashtable = new Hashtable();
                newObject = newHashtable ?? throw new NullReferenceException($"{nameof(newHashtable)} cannot be null");
                var hashtable  = (Hashtable)sourceObject;
                var success    = false;
                var retryCount = 0;
                while (!success && retryCount < 10)
                {
                    try
                    {
                        foreach (DictionaryEntry item in hashtable)
                        {
                            var key   = InspectAndCopy(item.Key, null, null, currentDepth, maxDepth, configuration, objectTree, path, ignorePropertiesOrPaths);
                            var value = InspectAndCopy(item.Value, null, null, currentDepth, maxDepth, configuration, objectTree, path, ignorePropertiesOrPaths);
                            newHashtable.Add(key, value);
                        }
                        success = true;
                    }
                    catch (InvalidOperationException)
                    {
                        // if the collection was modified during enumeration, stop re-initialize and retry
                        success = false;
                        retryCount++;
                        newHashtable.Clear();
                    }
                }
                if (!success)
                {
                    throw new CloneException($"Error cloning Hashtable at path '{path}'. Ensure the object is not modified while cloning data utilizing thread-safe access.");
                }
                return(newObject);
            }
            else if (typeSupport.IsEnumerable && !typeSupport.IsArray)
            {
                // clone enumerable elements
                var  enumerable = (IEnumerable)sourceObject;
                bool hasEntries;
                if (typeSupport.IsCollection)
                {
                    hasEntries = ((ICollection)sourceObject).Count > 0;
                }
                else
                {
                    hasEntries = enumerable.Cast <object>().Any();
                }
                if (hasEntries)
                {
#if NET45_OR_GREATER || NETSTANDARD1_0_OR_GREATER
                    var readOnlyCollectionTypes = new[] { typeof(ReadOnlyCollection <>), typeof(ReadOnlyDictionary <,>) };
                    if (type.IsGenericType && readOnlyCollectionTypes.Contains(type.GetGenericTypeDefinition()))
                    {
                        // return as-is, since they can't be modified anyways
                        return(sourceObject);
                    }
#endif

                    var addMethod = typeSupport.Type.GetMethod("Add");
                    if (addMethod == null)
                    {
                        addMethod = typeSupport.Type.GetMethod("Enqueue");
                        if (addMethod == null)
                        {
                            addMethod = typeSupport.Type.GetMethod("Push");
                            if (addMethod == null)
                            {
                                addMethod = typeSupport.Methods.FirstOrDefault(x => x.Name.StartsWith("Add"));
                                if (addMethod == null)
                                {
                                    // as a backup, try utilizing memberwise clone
                                    return(_memberwiseCloneMethod.Invoke(sourceObject, null));
                                }
                            }
                        }
                    }

                    var success    = false;
                    var retryCount = 0;
                    while (!success && retryCount < 10)
                    {
                        try
                        {
                            foreach (var item in enumerable)
                            {
                                var element = InspectAndCopy(item, null, null, currentDepth, maxDepth, configuration,
                                                             objectTree, path, ignorePropertiesOrPaths);
                                addMethod.Invoke(newObject, new[] { element });
                            }

                            success = true;
                        }
                        catch (InvalidOperationException)
                        {
                            // if the collection was modified during enumeration, stop re-initialize and retry
                            success = false;
                            retryCount++;
                            var clearMethod = typeSupport.Type.GetMethod("Clear");
                            clearMethod?.Invoke(newObject, null);
                        }
                    }
                    if (!success)
                    {
                        throw new CloneException($"Error cloning IEnumerable at path '{path}'. Ensure the object is not modified while cloning data utilizing thread-safe access.");
                    }
                }

                return(newObject);
            }

            // clone an arrays' elements
            if (typeSupport.IsArray)
            {
                var sourceArray = sourceObject as Array;
                var newArray    = newObject as Array;

                // performance optimization, value typed primitive arrays can be block copied
                if (typeSupport.ElementType.IsPrimitive)
                {
                    var bytesPerValue = GetBytesPerValue(typeSupport.ElementType);
                    try
                    {
                        Buffer.BlockCopy(sourceArray, 0, newArray, 0, sourceArray.Length * bytesPerValue);
                    }
                    catch (Exception ex)
                    {
                        throw new CloneException($"Error block copying array at path '{path}'", ex);
                    }
                }
                else
                {
                    // copy each array element and clone the value
                    var arrayRank       = newArray.Rank;
                    var arrayDimensions = new List <int>();
                    for (var dimension = 0; dimension < arrayRank; dimension++)
                    {
                        arrayDimensions.Add(newArray.GetLength(dimension));
                    }
                    var flatRowIndex = 0;
                    foreach (var row in sourceArray)
                    {
                        var newElement = InspectAndCopy(row, null, null, currentDepth, maxDepth, configuration, objectTree, path, ignorePropertiesOrPaths);
                        // performance optimization, skip dimensional processing if it's a 1d array
                        if (arrayRank > 1)
                        {
                            // this is an optimized multi-dimensional array reconstruction
                            // based on the formula: indices.Add((i / (arrayDimensions[arrayRank - 1] * arrayDimensions[arrayRank - 2] * arrayDimensions[arrayRank - 3] * arrayDimensions[arrayRank - 4] * arrayDimensions[arrayRank - 5])) % arrayDimensions[arrayRank - 6]);
                            var indices = new List <int>();
                            for (var r = 1; r <= arrayRank; r++)
                            {
                                var multi = 1;
                                for (var p = 1; p < r; p++)
                                {
                                    multi *= arrayDimensions[arrayRank - p];
                                }
                                var b = (flatRowIndex / multi) % arrayDimensions[arrayRank - r];
                                indices.Add(b);
                            }
                            indices.Reverse();
                            // set element of multi-dimensional array
                            newArray.SetValue(newElement, indices.ToArray());
                        }
                        else
                        {
                            // set element of 1d array
                            newArray.SetValue(newElement, flatRowIndex);
                        }
                        flatRowIndex++;
                    }
                }
                return(newArray);
            }

            if (typeSupport.IsExpression)
            {
                // utilize MemberwiseClone for expressions
                try
                {
                    var newExpression = _memberwiseCloneMethod.Invoke(sourceObject, null);
                    return(newExpression);
                }
                catch (Exception ex)
                {
                    throw new CloneException($"Error cloning expression with type '{typeSupport.FullName}' at path '{path}'", ex);
                }
            }

            var fields    = typeSupport.Fields.Where(x => !x.IsConstant && !x.IsStatic);
            var rootPath  = path;
            var localPath = string.Empty;
            // clone and recurse fields
            foreach (var field in fields)
            {
                localPath = $"{rootPath}.{field.Name}";
                // optimization to disable ignores by attribute if configured
                //System.Diagnostics.Debug.WriteLine($"Copying {localPath}");
                if (_hasIgnoreConfiguration.Value)
                {
                    if (IgnoreObjectName(field.Name, localPath, configuration, ignorePropertiesOrPaths, configuration.IgnorePropertiesWithAttributes.Count > 0 ? field.CustomAttributes : null))
                    {
                        continue;
                    }
                    // also check the property for ignore, if this is a auto-backing property
                    if (field.BackedProperty != null && IgnoreObjectName(field.BackedProperty.Name, $"{rootPath}.{field.BackedPropertyName}", configuration, ignorePropertiesOrPaths, configuration.IgnorePropertiesWithAttributes.Count > 0 ? field.BackedProperty.CustomAttributes : null))
                    {
                        continue;
                    }
                }
#if FEATURE_DISABLE_SET_INITONLY
                // we can't duplicate init-only fields since .net core 3.0+
                // make use of IL to get around this limitation
                if (field.FieldInfo.IsInitOnly && configuration.AllowCloningOfReadOnlyEntities)
                {
                    try
                    {
                        var updateFieldValue = sourceObject.GetFieldValue(field);
                        var updater          = GetWriterForField(field);
                        updater(ref newObject, updateFieldValue);
                    }
                    catch (Exception ex)
                    {
                        throw new CloneException($"Failed to set field value named '{field.Name}' at path '{path}' using IL DynamicMethod", ex);
                    }
                    continue;
                }
#endif
                // only copy readonly fields if we allow as such
                if (field.FieldInfo.IsInitOnly && !configuration.AllowCloningOfReadOnlyEntities)
                {
                    continue;
                }

                // utilize reflection
                var fieldTypeSupport = field.Type;
                var fieldValue       = sourceObject.GetFieldValue(field);
                var destinationField = newObject.GetField(field.Name, true);
                // does this field exist on the destination object with the same type?
                if (destinationField != null && destinationField.FieldType == field.FieldInfo.FieldType)
                {
                    if (fieldTypeSupport.IsValueType || fieldTypeSupport.IsImmutable)
                    {
                        SetFieldValue(newObject, destinationField, fieldValue, localPath);
                    }
                    else if (fieldValue != null)
                    {
                        var clonedFieldValue = InspectAndCopy(fieldValue, null, null, currentDepth, maxDepth,
                                                              configuration, objectTree, localPath, ignorePropertiesOrPaths);
                        SetFieldValue(newObject, destinationField, clonedFieldValue, localPath);
                    }
                }
            }

            return(newObject);
        }