/// <summary> /// Clone any object /// </summary> /// <param name="sourceObject">The object to clone</param> /// <param name="options">The cloning options</param> /// <param name="maxTreeDepth">The maximum tree depth</param> /// <returns></returns> public T Clone(T sourceObject, CloneOptions options, int maxTreeDepth, params Expression <Func <T, object> >[] ignoreProperties) { var configuration = CloneConfiguration.CreateFromOptions(options); return((T)InspectAndCopy(sourceObject, null, null, 0, maxTreeDepth, configuration, new ObjectTreeReferenceTracker(configuration.ReferenceTrackingType, configuration.AllowUseCustomHashCodes), string.Empty, ConvertToPropertyNameList(ignoreProperties))); }
/// <summary> /// Returns true if an ignore configuration is configured /// </summary> /// <param name="configuration"></param> /// <param name="ignorePropertiesOrPaths"></param> /// <returns></returns> private bool HasIgnoreConfiguration(CloneConfiguration configuration, ICollection <string> ignorePropertiesOrPaths) { if (configuration.IgnorePropertiesWithAttributes.Count == 0 && (ignorePropertiesOrPaths == null || ignorePropertiesOrPaths.Count == 0)) { return(false); } return(true); }
/// <summary> /// Returns true if object name should be ignored /// </summary> /// <param name="name">Property or field name</param> /// <param name="path">Full path to object</param> /// <param name="configuration">Configure custom cloning options</param> /// <param name="ignorePropertiesOrPaths">List of names or paths to ignore</param> /// <returns></returns> private bool IgnoreObjectName(string name, string path, CloneConfiguration configuration, ICollection <string> ignorePropertiesOrPaths, IEnumerable <CustomAttributeData> attributes = null) { var ignoreByNameOrPath = ignorePropertiesOrPaths?.Contains(name) == true || ignorePropertiesOrPaths?.Contains(path) == true; if (ignoreByNameOrPath) { return(true); } #if FEATURE_CUSTOM_ATTRIBUTES if (attributes?.Any(x => configuration.IgnorePropertiesWithAttributes?.Contains(x.AttributeType.Name) == true) == true) #else if (attributes?.Any(x => configuration.IgnorePropertiesWithAttributes?.Contains(x.Constructor.DeclaringType.Name) == true) == true) #endif { return(true); } return(false); }
/// <summary> /// Clone any object /// </summary> /// <typeparam name="T"></typeparam> /// <param name="sourceObject">The object to clone</param> /// <param name="configuration">Configure custom cloning options</param> /// <param name="ignorePropertiesOrPaths">A list of property names or fully qualified paths to ignore</param> /// <returns></returns> public static T Clone <T>(this T sourceObject, CloneConfiguration configuration, params string[] ignorePropertiesOrPaths) { var cloneProvider = new CloneProvider <T>(); return(cloneProvider.Clone(sourceObject, configuration, configuration?.MaxDepth ?? CloneConfiguration.DefaultMaxDepth, ignorePropertiesOrPaths)); }
/// <summary> /// Clone any object /// </summary> /// <param name="sourceObject">The object to clone</param> /// <param name="configuration">Configure custom cloning options</param> /// <param name="maxTreeDepth">The maximum tree depth</param> /// <returns></returns> public T Clone(T sourceObject, CloneConfiguration configuration, int maxTreeDepth, params string[] ignorePropertiesOrPaths) => (T)InspectAndCopy(sourceObject, null, null, 0, maxTreeDepth, configuration, new ObjectTreeReferenceTracker(configuration.ReferenceTrackingType, configuration.AllowUseCustomHashCodes), string.Empty, ignorePropertiesOrPaths);
/// <summary> /// Clone any object /// </summary> /// <param name="sourceObject">The object to clone</param> /// <param name="options">The cloning options</param> /// <param name="maxTreeDepth">The maximum tree depth</param> /// <returns></returns> public T Clone(T sourceObject, CloneOptions options, int maxTreeDepth, params string[] ignorePropertiesOrPaths) { var configuration = CloneConfiguration.CreateFromOptions(options); return((T)InspectAndCopy(sourceObject, null, null, 0, maxTreeDepth, configuration, new ObjectTreeReferenceTracker(configuration.ReferenceTrackingType, configuration.AllowUseCustomHashCodes), string.Empty, ignorePropertiesOrPaths)); }
/// <summary> /// Clone any object /// </summary> /// <param name="sourceObject">The object to clone</param> /// <param name="options">The cloning options</param> /// <param name="maxTreeDepth">The maximum tree depth</param> /// <returns></returns> public T Clone(T sourceObject, CloneOptions options, int maxTreeDepth) { var configuration = CloneConfiguration.CreateFromOptions(options); return((T)InspectAndCopy(sourceObject, 0, maxTreeDepth, configuration, new ObjectTreeReferenceTracker(configuration.ReferenceTrackingType, configuration.AllowUseCustomHashCodes), string.Empty)); }
/// <summary> /// Clone any object /// </summary> /// <param name="sourceObject">The object to clone</param> /// <param name="configuration">Configure custom cloning options</param> /// <returns></returns> public T Clone(T sourceObject, CloneConfiguration configuration) { return((T)InspectAndCopy(sourceObject, 0, configuration?.MaxDepth ?? CloneConfiguration.DefaultMaxDepth, configuration, new ObjectTreeReferenceTracker(configuration.ReferenceTrackingType, configuration.AllowUseCustomHashCodes), string.Empty)); }
/// <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);
/// <summary> /// Clone any object to another type and provide an existing instance to clone to /// </summary> /// <typeparam name="TIn">The type to clone from</typeparam> /// <typeparam name="TOut">The type to clone to</typeparam> /// <param name="sourceObject"></param> /// <param name="destinationInstance">The existing instance to clone to</param> /// <param name="configuration">Configure custom cloning options</param> /// <returns></returns> public TOut CloneTo <TIn, TOut>(TIn sourceObject, TOut destinationInstance, CloneConfiguration configuration) => (TOut)InspectAndCopy(sourceObject, destinationInstance, typeof(TOut), 0, configuration?.MaxDepth ?? CloneConfiguration.DefaultMaxDepth, configuration, new ObjectTreeReferenceTracker(configuration.ReferenceTrackingType, configuration.AllowUseCustomHashCodes), string.Empty, null);
/// <summary> /// Clone any object to another type /// </summary> /// <typeparam name="TIn">The type to clone from</typeparam> /// <typeparam name="TOut">The type to clone to</typeparam> /// <param name="sourceObject"></param> /// <returns></returns> public TOut CloneTo <TIn, TOut>(TIn sourceObject) { var configuration = new CloneConfiguration(); return((TOut)InspectAndCopy(sourceObject, null, typeof(TOut), 0, CloneConfiguration.DefaultMaxDepth, configuration, new ObjectTreeReferenceTracker(configuration.ReferenceTrackingType, configuration.AllowUseCustomHashCodes), string.Empty, null)); }
/// <summary> /// Clone any object /// </summary> /// <typeparam name="T"></typeparam> /// <param name="sourceObject">The object to clone</param> /// <param name="configuration">Configure custom cloning options</param> /// <returns></returns> public static T Clone <T>(this T sourceObject, CloneConfiguration configuration) { var cloneProvider = new CloneProvider <T>(); return(cloneProvider.Clone(sourceObject, configuration)); }
/// <summary> /// Clone any object /// </summary> /// <typeparam name="T"></typeparam> /// <param name="sourceObject">The object to clone</param> /// <param name="options">Specify the Clone options</param> /// <returns></returns> public static T Clone <T>(this T sourceObject, CloneOptions options) { var cloneProvider = new CloneProvider <T>(); return(cloneProvider.Clone(sourceObject, CloneConfiguration.CreateFromOptions(options))); }
/// <summary> /// Clone any object to another type and provide an existing instance to clone to /// </summary> /// <typeparam name="TIn"></typeparam> /// <typeparam name="TOut"></typeparam> /// <param name="sourceObject"></param> /// <param name="destinationInstance"></param> /// <param name="configuration">Configure custom cloning options</param> /// <returns></returns> public static TOut CloneTo <TIn, TOut>(this TIn sourceObject, TOut destinationInstance, CloneConfiguration configuration) { var cloneProvider = new CloneProvider <TIn>(); return(cloneProvider.CloneTo(sourceObject, destinationInstance, configuration)); }
/// <summary> /// Clone any object to another type /// </summary> /// <typeparam name="TOut"></typeparam> /// <param name="sourceObject"></param> /// <param name="configuration">Configure custom cloning options</param> /// <returns></returns> public static TOut CloneTo <TOut>(this object sourceObject, CloneConfiguration configuration) { var cloneProvider = new CloneProvider <object>(); return(cloneProvider.CloneTo <object, TOut>(sourceObject, configuration)); }
/// <summary> /// Clone any object /// </summary> /// <typeparam name="T"></typeparam> /// <param name="sourceObject">The object to clone</param> /// <param name="configuration">Configure custom cloning options</param> /// <param name="ignoreProperties">A list of property names to ignore</param> /// <returns></returns> public static T Clone <T>(this T sourceObject, CloneConfiguration configuration, params Expression <Func <T, object> >[] ignoreProperties) { var cloneProvider = new CloneProvider <T>(); return(cloneProvider.Clone(sourceObject, configuration, configuration?.MaxDepth ?? CloneConfiguration.DefaultMaxDepth, ignoreProperties)); }
/// <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); }