| | | 1 | | // ----------------------------------------------------------------------- |
| | | 2 | | // <copyright file="ObjectGraphMapper.cs" company="Stéphane ANDRE"> |
| | | 3 | | // Copyright (c) Stéphane ANDRE. All rights reserved. |
| | | 4 | | // </copyright> |
| | | 5 | | // ----------------------------------------------------------------------- |
| | | 6 | | |
| | | 7 | | using System; |
| | | 8 | | using System.Collections.Generic; |
| | | 9 | | using System.Reflection; |
| | | 10 | | |
| | | 11 | | namespace MyNet.Reflection; |
| | | 12 | | |
| | | 13 | | internal static class ObjectGraphMapper |
| | | 14 | | { |
| | 3 | 15 | | private static readonly MethodInfo CloneMethod = typeof(object).GetMethod("MemberwiseClone", BindingFlags.Instance | |
| | | 16 | | |
| | | 17 | | /// <summary> |
| | | 18 | | /// Creates a deep clone of the specified object, recursively copying all fields and properties. If the <paramref na |
| | | 19 | | /// </summary> |
| | | 20 | | /// <param name="obj">The object to clone.</param> |
| | | 21 | | /// <param name="options">The options for deep cloning.</param> |
| | | 22 | | /// <typeparam name="T">The type of the object to clone.</typeparam> |
| | | 23 | | /// <returns>A deep clone of the specified object.</returns> |
| | | 24 | | public static T? Clone<T>(T? obj, DeepCloneOptions? options = null) |
| | | 25 | | { |
| | 9 | 26 | | if (obj is null) |
| | 0 | 27 | | return default; |
| | | 28 | | |
| | 9 | 29 | | var visited = ShouldPreserveReferences(options) |
| | 9 | 30 | | ? new Dictionary<object, object?>(ReferenceEqualityComparer.Instance) |
| | 9 | 31 | | : null; |
| | | 32 | | |
| | 9 | 33 | | return (T?)CloneInternal(obj, visited); |
| | | 34 | | } |
| | | 35 | | |
| | | 36 | | /// <summary> |
| | | 37 | | /// Recursively clones the specified object, handling reference preservation if a visited dictionary is provided. Th |
| | | 38 | | /// </summary> |
| | | 39 | | /// <param name="obj">The object to clone.</param> |
| | | 40 | | /// <param name="visited">A dictionary to track visited objects for reference preservation.</param> |
| | | 41 | | /// <returns>A deep clone of the specified object.</returns> |
| | | 42 | | private static object? CloneInternal(object? obj, IDictionary<object, object?>? visited) |
| | | 43 | | { |
| | 72 | 44 | | if (obj is null) |
| | 0 | 45 | | return null; |
| | | 46 | | |
| | 72 | 47 | | var type = obj.GetType(); |
| | | 48 | | |
| | 72 | 49 | | if (type.IsSimple()) |
| | 36 | 50 | | return obj; |
| | | 51 | | |
| | 36 | 52 | | if (visited != null && visited.TryGetValue(obj, out var existing)) |
| | 12 | 53 | | return existing; |
| | | 54 | | |
| | 24 | 55 | | if (obj is Delegate) |
| | 0 | 56 | | return obj; |
| | | 57 | | |
| | 24 | 58 | | var clone = CloneMethod.Invoke(obj, null); |
| | | 59 | | |
| | 24 | 60 | | visited?[obj] = clone; |
| | | 61 | | |
| | 24 | 62 | | if (obj is Array sourceArray && clone is Array clonedArray) |
| | | 63 | | { |
| | 0 | 64 | | CopyArrayElements(sourceArray, clonedArray, visited); |
| | 0 | 65 | | return clone; |
| | | 66 | | } |
| | | 67 | | |
| | 24 | 68 | | CopyFields(type, obj, clone, visited); |
| | | 69 | | |
| | 24 | 70 | | return clone; |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | /// <summary> |
| | | 74 | | /// Copies the fields of the specified type from the source object to the target object, recursively cloning field v |
| | | 75 | | /// </summary> |
| | | 76 | | /// <param name="type">The type whose fields are to be copied.</param> |
| | | 77 | | /// <param name="source">The source object from which to copy field values.</param> |
| | | 78 | | /// <param name="target">The target object to which field values are copied.</param> |
| | | 79 | | /// <param name="visited">A dictionary to track visited objects for reference preservation.</param> |
| | | 80 | | private static void CopyFields(Type? type, object source, object? target, IDictionary<object, object?>? visited) |
| | | 81 | | { |
| | 81 | 82 | | while (type != null) |
| | | 83 | | { |
| | 54 | 84 | | var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); |
| | | 85 | | |
| | 180 | 86 | | foreach (var field in fields) |
| | | 87 | | { |
| | 36 | 88 | | if (field.IsDefined(typeof(NonSerializedAttribute), true)) |
| | | 89 | | continue; |
| | | 90 | | |
| | 36 | 91 | | var value = field.GetValue(source); |
| | 36 | 92 | | var cloned = CloneInternal(value, visited); |
| | | 93 | | |
| | 36 | 94 | | field.SetValue(target, cloned); |
| | | 95 | | } |
| | | 96 | | |
| | 54 | 97 | | type = type.BaseType; |
| | | 98 | | } |
| | 27 | 99 | | } |
| | | 100 | | |
| | | 101 | | /// <summary> |
| | | 102 | | /// Recursively copies the elements of an array from the source to the target, cloning each element as needed while |
| | | 103 | | /// </summary> |
| | | 104 | | /// <param name="source">The source array whose values are copied.</param> |
| | | 105 | | /// <param name="target">The target array that receives the cloned values.</param> |
| | | 106 | | /// <param name="visited">A dictionary to track visited objects for reference preservation.</param> |
| | | 107 | | private static void CopyArrayElements(Array source, Array target, IDictionary<object, object?>? visited) |
| | | 108 | | { |
| | 9 | 109 | | if (source.Rank != target.Rank) |
| | 0 | 110 | | throw new ArgumentException("Source and target arrays must have the same rank.", nameof(target)); |
| | | 111 | | |
| | 9 | 112 | | var indices = new int[source.Rank]; |
| | | 113 | | |
| | 9 | 114 | | copyDimension(0); |
| | 9 | 115 | | return; |
| | | 116 | | |
| | | 117 | | void copyDimension(int dimension) |
| | | 118 | | { |
| | | 119 | | var sourceLowerBound = source.GetLowerBound(dimension); |
| | | 120 | | var sourceUpperBound = source.GetUpperBound(dimension); |
| | | 121 | | |
| | | 122 | | if (sourceLowerBound != target.GetLowerBound(dimension) || sourceUpperBound != target.GetUpperBound(dimensio |
| | | 123 | | throw new ArgumentException("Source and target arrays must have the same bounds.", nameof(target)); |
| | | 124 | | |
| | | 125 | | for (var i = sourceLowerBound; i <= sourceUpperBound; i++) |
| | | 126 | | { |
| | | 127 | | indices[dimension] = i; |
| | | 128 | | |
| | | 129 | | if (dimension < source.Rank - 1) |
| | | 130 | | { |
| | | 131 | | copyDimension(dimension + 1); |
| | | 132 | | } |
| | | 133 | | else |
| | | 134 | | { |
| | | 135 | | var value = source.GetValue(indices); |
| | | 136 | | var cloned = CloneInternal(value, visited); |
| | | 137 | | target.SetValue(cloned, indices); |
| | | 138 | | } |
| | | 139 | | } |
| | | 140 | | } |
| | | 141 | | } |
| | | 142 | | |
| | | 143 | | /// <summary> |
| | | 144 | | /// Recursively populates the fields of the destination object with values from the source object, handling referenc |
| | | 145 | | /// </summary> |
| | | 146 | | /// <param name="source">The source object from which to copy field values.</param> |
| | | 147 | | /// <param name="destination">The destination object to which field values are copied.</param> |
| | | 148 | | /// <param name="options">Options to control the cloning behavior.</param> |
| | | 149 | | public static void Populate(object source, object destination, DeepCloneOptions? options = null) |
| | | 150 | | { |
| | 12 | 151 | | ArgumentNullException.ThrowIfNull(source); |
| | 12 | 152 | | ArgumentNullException.ThrowIfNull(destination); |
| | | 153 | | |
| | 12 | 154 | | var visited = ShouldPreserveReferences(options) |
| | 12 | 155 | | ? new Dictionary<object, object?>(ReferenceEqualityComparer.Instance) |
| | 12 | 156 | | : null; |
| | | 157 | | |
| | 12 | 158 | | PopulateInternal(source, destination, visited); |
| | 12 | 159 | | } |
| | | 160 | | |
| | | 161 | | /// <summary> |
| | | 162 | | /// Recursively populates the fields of the destination object with values from the source object, handling referenc |
| | | 163 | | /// </summary> |
| | | 164 | | /// <param name="source">The source object from which to copy field values.</param> |
| | | 165 | | /// <param name="target">The target object to which field values are copied.</param> |
| | | 166 | | /// <param name="visited">A dictionary to track visited objects for reference preservation.</param> |
| | | 167 | | public static void PopulateInternal(object source, object target, IDictionary<object, object?>? visited) |
| | | 168 | | { |
| | 12 | 169 | | var type = source.GetType(); |
| | | 170 | | |
| | 12 | 171 | | if (type.IsSimple()) |
| | 0 | 172 | | return; |
| | | 173 | | |
| | 12 | 174 | | if (visited?.TryGetValue(source, out _) == true) |
| | 0 | 175 | | return; |
| | | 176 | | |
| | 12 | 177 | | if (source is Delegate) |
| | 0 | 178 | | return; |
| | | 179 | | |
| | 12 | 180 | | visited?[source] = target; |
| | | 181 | | |
| | 12 | 182 | | if (source is Array sourceArray && target is Array targetArray) |
| | | 183 | | { |
| | 9 | 184 | | CopyArrayElements(sourceArray, targetArray, visited); |
| | 9 | 185 | | return; |
| | | 186 | | } |
| | | 187 | | |
| | 3 | 188 | | CopyFields(type, source, target, visited); |
| | 3 | 189 | | } |
| | | 190 | | |
| | 21 | 191 | | private static bool ShouldPreserveReferences(DeepCloneOptions? options) => options?.PreserveReferences ?? true; |
| | | 192 | | } |
| | | 193 | | |