GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
ObjectCopier.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Linq;
20using System.Reflection;
21using System.Collections;
22using static GenerallyPositive.Enums;
23using System.Runtime.CompilerServices;
24
25namespace GenerallyPositive
26{
27 public class ReferenceEqualityComparer : IEqualityComparer<object>
28 {
29 public new bool Equals(object x, object y) => ReferenceEquals(x, y);
30 public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
31 }
32
37 internal static class ObjectCopier
38 {
39 public static T DeepCopyObject<T>(dynamic source, T destination, DeepCopy deepCopy, HashSet<object> visited = null)
40 {
41 if (source == null || destination == null)
42 {
43 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Source and/or destination must not be null. Returning whichever is not null or default(T).", null, GPALObjectType.None);
44 return null != source ? source : null != destination ? destination : default(T);
45 }
46
47 visited = visited ?? new HashSet<object>(new ReferenceEqualityComparer());
48 if (!visited.Add(source))
49 {
50 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Circular reference detected in DeepCopyObject @ [{source}].", source, GPALObjectType.None);
51 return destination;
52 }
53
54 Type sourceType = source.GetType();
55
56 // Handle dictionaries
57 if (typeof(IDictionary).IsAssignableFrom(sourceType))
58 {
59 if (!typeof(IDictionary).IsAssignableFrom(destination.GetType()))
60 {
61 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Destination [{destination.GetType()}] is not a compatible [{sourceType}] dictionary type.", source, GPALObjectType.None);
62 return destination;
63 }
64 CopyDictionary((IDictionary)source, (IDictionary)destination, deepCopy, visited);
65 }
66 // Handle enumerables (lists, arrays, etc.)
67 else if (typeof(IEnumerable).IsAssignableFrom(sourceType) && !IsImmutableType(sourceType))
68 {
69 if (!typeof(IList).IsAssignableFrom(destination.GetType()))
70 {
71 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Destination [{destination.GetType()}] is not a compatible list type.", source, GPALObjectType.None);
72 return destination;
73 }
74 CopyObjects((IEnumerable)source, (IList)destination, deepCopy, visited);
75 }
76 // Handle single objects
77 else
78 {
79 destination = CopyMembers(source, destination, deepCopy, visited);
80 }
81
82 return destination;
83 }
84
85 static void CopyDictionary(IDictionary sourceDict, IDictionary destinationDict, DeepCopy deepCopy, HashSet<object> visited)
86 {
87 destinationDict.Clear(); // Clear the destination to avoid duplicate entries
88
89 foreach (var key in sourceDict.Keys)
90 {
91 // Sanitize dynamic key to prevent misinterpretation as culture identifier
92 object safeKey;
93 if (key is string strKey)
94 safeKey = strKey;
95 else if (key is int intKey)
96 safeKey = intKey;
97 else if (key is null)
98 safeKey = null;
99 else
100 {
101 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Unsupported dynamic key type [{key.GetType().Name}]. Using ToString as fallback.", key, GPALObjectType.None);
102 safeKey = key.ToString();
103 }
104
105 var sourceValue = sourceDict[key];
106 if (sourceValue == null)
107 {
108 destinationDict[safeKey] = null;
109 continue;
110 }
111
112 Type valueType = sourceValue.GetType();
113
114 destinationDict[safeKey] = sourceValue;
115 }
116 }
117
118 static IList CopyObjects(IEnumerable sourceObjects, IList destinationObjects, DeepCopy deepCopy, HashSet<object> visited)
119 {
120 destinationObjects.Clear(); // Clear to avoid duplicates
121 foreach (var source in sourceObjects)
122 {
123 if (source == null)
124 {
125 destinationObjects.Add(null);
126 continue;
127 }
128
129 var destination = ConverterHelper.InstantiateOne(source.GetType());
130 destination = DeepCopyObject(source, destination, deepCopy, visited); // Pass visited
131 destinationObjects.Add(destination);
132 }
133 return destinationObjects;
134 }
135
136 public static T CopyMembers<T>(object source, T destination, DeepCopy deepCopy, HashSet<object> visited)
137 {
138 if (source == null || destination == null)
139 {
140 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Source or destination is null in CopyMembers.", null, GPALObjectType.None);
141 return destination;
142 }
143
144 Type sourceType = source.GetType();
145 Type destType = destination.GetType();
146
147 // Handle specific types like IGPALGrid<string>
148 if (source is IGPALGrid<string>)
149 {
150 return (T)sourceType.GetMethod("Clone")?.Invoke(source, null) ?? destination;
151 }
152
153 // Copy fields if specified
154 if (deepCopy == DeepCopy.Fields || deepCopy == DeepCopy.FieldsAndProperties)
155 {
156 FieldInfo[] fields = sourceType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
157 foreach (FieldInfo field in fields)
158 {
159 if (IsSystemInternalType(field.FieldType) || IsCultureRelatedType(field.FieldType))
160 {
161 continue;
162 }
163
164 try
165 {
166 object sourceValue = field.GetValue(source);
167 if (sourceValue == null)
168 {
169 field.SetValue(destination, null);
170 continue;
171 }
172
173 Type fieldType = field.FieldType;
174
175 if (typeof(IEnumerable).IsAssignableFrom(fieldType) && !IsImmutableType(fieldType))
176 {
177 object destEnumerable = ConverterHelper.InstantiateOne(fieldType);
178 if (destEnumerable == null)
179 {
180 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Failed to instantiate enumerable type [{fieldType.Name}].", source, GPALObjectType.None);
181 continue;
182 }
183
184 if (typeof(IDictionary).IsAssignableFrom(fieldType))
185 {
186 CopyDictionary((IDictionary)sourceValue, (IDictionary)destEnumerable, deepCopy, visited);
187 }
188 else if (typeof(IList).IsAssignableFrom(fieldType))
189 {
190 CopyObjects((IEnumerable)sourceValue, (IList)destEnumerable, deepCopy, visited);
191 }
192 else
193 {
194 CopyGenericEnumerable((IEnumerable)sourceValue, destEnumerable, fieldType, deepCopy, visited);
195 }
196
197 field.SetValue(destination, destEnumerable);
198 }
199 else if (!fieldType.IsValueType && !IsImmutableType(fieldType) && !IsSystemInternalType(fieldType) && !IsCultureRelatedType(fieldType))
200 {
201 object destValue = ConverterHelper.InstantiateOne(fieldType);
202 destValue = DeepCopyObject(sourceValue, destValue, deepCopy, visited); // Pass visited
203 field.SetValue(destination, destValue);
204 }
205 else
206 {
207 field.SetValue(destination, sourceValue); // Shallow copy
208 }
209 }
210 catch (Exception ex)
211 {
212 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to copy field [{field.Name}] of type [{sourceType.Name}].", source, GPALObjectType.Other, ex);
213 }
214 }
215 }
216
217 // Copy properties if specified
218 if (deepCopy == DeepCopy.Properties || deepCopy == DeepCopy.FieldsAndProperties)
219 {
220 PropertyInfo[] properties = sourceType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
221 foreach (PropertyInfo property in properties)
222 {
223 if (property.GetIndexParameters().Length > 0 || // Skip indexers
224 property.GetCustomAttributes(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false).Length > 0 || // Skip compiler-generated properties
225 (property.DeclaringType != null && (typeof(ICollection).IsAssignableFrom(property.DeclaringType) ||
226 IsSystemInternalType(property.DeclaringType) ||
227 IsCultureRelatedType(property.DeclaringType))))
228 {
229 continue;
230 }
231
232 try
233 {
234 if (property.CanRead && property.CanWrite)
235 {
236 object sourceValue = property.GetValue(source);
237 if (sourceValue == null)
238 {
239 property.SetValue(destination, null);
240 continue;
241 }
242
243 Type propType = property.PropertyType;
244
245 if (typeof(IEnumerable).IsAssignableFrom(propType) && !IsImmutableType(propType))
246 {
247 object destEnumerable = ConverterHelper.InstantiateOne(propType);
248 if (destEnumerable == null)
249 {
250 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Failed to instantiate enumerable type [{propType.Name}].", source, GPALObjectType.None);
251 continue;
252 }
253
254 if (typeof(IDictionary).IsAssignableFrom(propType))
255 {
256 CopyDictionary((IDictionary)sourceValue, (IDictionary)destEnumerable, deepCopy, visited);
257 }
258 else if (typeof(IList).IsAssignableFrom(propType))
259 {
260 CopyObjects((IEnumerable)sourceValue, (IList)destEnumerable, deepCopy, visited);
261 }
262 else
263 {
264 CopyGenericEnumerable((IEnumerable)sourceValue, destEnumerable, propType, deepCopy, visited);
265 }
266
267 property.SetValue(destination, destEnumerable);
268 }
269 else if (!propType.IsValueType && !IsImmutableType(propType) && !IsSystemInternalType(propType) && !IsCultureRelatedType(propType))
270 {
271 object destValue = ConverterHelper.InstantiateOne(propType);
272 destValue = DeepCopyObject(sourceValue, destValue, deepCopy, visited); // Pass visited
273 property.SetValue(destination, destValue);
274 }
275 else
276 {
277 property.SetValue(destination, sourceValue); // Shallow copy
278 }
279 }
280 }
281 catch (Exception ex)
282 {
283 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to copy property [{property.Name}] of type [{sourceType.Name}].", source, GPALObjectType.Other, ex);
284 }
285 }
286 }
287
288 return destination;
289 }
290
291 private static void CopyGenericEnumerable(IEnumerable sourceEnumerable, object destEnumerable, Type enumerableType, DeepCopy deepCopy, HashSet<object> visited)
292 {
293 MethodInfo addMethod = enumerableType.GetMethod("Add");
294 if (addMethod == null)
295 {
296 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"No Add method found for type [{enumerableType.Name}].", null, GPALObjectType.None);
297 return;
298 }
299
300 foreach (var item in sourceEnumerable)
301 {
302 if (item == null)
303 {
304 addMethod.Invoke(destEnumerable, new object[] { null });
305 continue;
306 }
307
308 Type itemType = item.GetType();
309 object destItem;
310
311 if (!itemType.IsValueType && !IsImmutableType(itemType) && !IsSystemInternalType(itemType) && !IsCultureRelatedType(itemType))
312 {
313 if (!visited.Add(item))
314 {
315 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Circular reference detected for item of type [{itemType.Name}].", item, GPALObjectType.None);
316 continue;
317 }
318
319 destItem = ConverterHelper.InstantiateOne(itemType);
320 destItem = DeepCopyObject(item, destItem, deepCopy, visited); // Pass visited
321 }
322 else
323 {
324 destItem = item; // Shallow copy
325 }
326
327 addMethod.Invoke(destEnumerable, new object[] { destItem });
328 }
329 }
330
331 private static bool IsImmutableType(Type type)
332 {
333 return type == typeof(string) ||
334 type == typeof(DateTime) ||
335 type == typeof(TimeSpan) ||
336 type == typeof(decimal) ||
337 type == typeof(Guid);
338 }
339
340 private static bool IsSystemInternalType(Type type)
341 {
342 return type.Namespace != null &&
343 (type.Namespace.StartsWith("System.Reflection") ||
344 type.Namespace.StartsWith("System.Runtime") ||
345 type.Namespace.StartsWith("System.Threading") ||
346 type.Namespace.StartsWith("System.Security"));
347 }
348
349 private static bool IsCultureRelatedType(Type type)
350 {
351 return type == typeof(System.Globalization.CultureInfo) ||
352 type.Namespace == "System.Globalization";
353 }
354
355 public static int GetItemCount(dynamic propertyOrField, dynamic source)
356 {
357 int count = 0;
358
359 if (source is IEnumerable<object> objectEnumerable)
360 {
361 return objectEnumerable.Count();
362 }
363 else if (source is Array array)
364 {
365 return array.Length;
366 }
367 else if (source is ICollection collection)
368 {
369 return collection.Count;
370 }
371 else if (null != propertyOrField)
372 {
373 dynamic enumeration = propertyOrField.GetValue(source);
374 if (null != enumeration)
375 foreach (dynamic item in enumeration)
376 count++;
377 return count;
378 }
379 else
380 {
381 try
382 {
383 foreach (var item in source)
384 count++;
385 }
386 catch (Exception)
387 { }
388 return count;
389 }
390 }
391
392 static bool IsPropertyEnumerable(PropertyInfo property)
393 {
394 return typeof(IEnumerable).IsAssignableFrom(property.PropertyType);
395 }
396
397 static bool IsFieldEnumerable(FieldInfo field)
398 {
399 return typeof(IEnumerable).IsAssignableFrom(field.FieldType);
400 }
401 }
402}