GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
ConverterHelper.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 DocumentFormat.OpenXml.Drawing.Diagrams;
18using DocumentFormat.OpenXml.Spreadsheet;
19using HtmlAgilityPack;
20using Microsoft.VisualBasic.FileIO;
21using System;
22using System.Collections;
23using System.Collections.Generic;
24using System.ComponentModel;
25using System.Globalization;
26using System.IO;
27using System.Linq;
28using System.Net;
29using System.Numerics;
30using System.Reflection;
31using System.Text;
32using System.Text.RegularExpressions;
33using System.Web;
34using System.Windows.Forms;
35using System.Xml;
36using YamlDotNet.Core;
37using static GenerallyPositive.Enums;
38
39namespace GenerallyPositive
40{
42 {
43 internal static ConverterSettings ConverterSettings { get; set; }
44 internal static Dictionary<Type, MethodInfo> ToStringCache { get; set; } = new Dictionary<Type, MethodInfo>();
45
46 public static List<dynamic> ConvertClassToList(dynamic myClass)
47 {
48 Type myClassType = myClass.GetType();
49 PropertyInfo[] properties = myClassType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
50 FieldInfo[] fields = myClassType.GetFields(BindingFlags.Public | BindingFlags.Instance);
51 List<dynamic> list = new List<dynamic>();
52 List<dynamic> myList;
53
54 // first check the object itself for collections
55 // we don't want to iterate properties of collection
56 // just on classes
57 if (IsDictionaryType(myClassType))
58 {
59 int idx = 0;
60 foreach (KeyValuePair<dynamic, dynamic> item in myClass)
61 {
62 Type elementType = item.GetType();
63
64 if (true == IsSimpleType(elementType))
65 list.Add(item.Value);
66 else
67 {
68 myList = ConvertClassToList(item.Value);
69 list.Add(myList);
70 }
71 idx++;
72 }
73 return list;
74 }
75 else if (IsEnumerableType(myClassType))
76 {
77 int idx = 0;
78 foreach (var item in myClass)
79 {
80 Type elementType = item.GetType();
81
82 if (true == IsSimpleType(elementType))
83 list.Add(item);
84 else
85 {
86 myList = ConvertClassToList(item);
87 list.Add(myList);
88 }
89 idx++;
90 }
91 return list;
92 }
93 else if (true == myClassType.IsArray) // must before checking for enumerable, arrays are enumerable but will not convert like enums - NOTE: ok, so did i find a way to not use this?
94 {
95 var arrayList = myClass as Array;
96 int len = arrayList.Length;
97
98 for (int idx = 0; idx < len; idx++)
99 {
100 var elementValue = arrayList.GetValue(idx);
101 var elementType = elementValue.GetType();
102
103 if (true == IsSimpleType(elementType))
104 list.Add(elementValue);
105 else
106 {
107 myList = ConvertClassToList(elementValue);
108 list.Add(myList);
109 }
110 }
111 return list;
112 }
113
114 if (null != properties)
115 foreach (PropertyInfo property in properties)
116 {
117 // skip indexers, property but nothing to read
118 if (property.GetIndexParameters().Length > 0 || // Indexer property
119 property.DeclaringType != null && property.DeclaringType.Namespace.StartsWith("System.Collections"))
120 {
121 continue; // Skip without logging
122 }
123
124 Type propertyType = property.PropertyType;
125
126 if (property.CanRead)
127 {
128 dynamic value = null;
129
130 if (true == propertyType.IsArray) // must be before checking for enumerable, arrays are enumerable but will not convert like enums
131 {
132 var arrayList = property.GetValue(myClass) as Array;
133 int len = arrayList.Length;
134
135 for (int idx = 0; idx < len; idx++)
136 {
137 var elementValue = arrayList.GetValue(idx);
138 var elementType = elementValue.GetType();
139
140 if (true == IsSimpleType(elementType))
141 list.Add(elementValue);
142 else if (true == IsEnumerableType(elementType))
143 {
144 IEnumerable enumeration = (IEnumerable)elementValue;
145 list.Add(enumeration);
146 }
147 else if (true == IsDictionaryType(elementType))
148 {
149 myList = ConvertClassToList(elementValue);
150 list.Add(myList);
151 }
152 }
153 }
154 else
155 {
156 try
157 {
158 value = property.GetValue(myClass);
159 }
160 catch (Exception ex)
161 {
162 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to getvalue of property [{property.Name}][{propertyType}]. Continuing and hoping for the best.", myClass, GPALObjectType.Other, ex);
163 }
164
165 if (value != null)
166 {
167 if (IsBigIntegerType(propertyType) || IsVersionType(propertyType))
168 {
169 list.Add(value.ToString());
170 }
171 else if (IsSimpleType(propertyType))
172 {
173 list.Add(ConvertValue(value, propertyType));
174 }
175 else if (IsDictionaryType(propertyType))
176 {
177 list.Add(ConvertClassToList(value));
178 }
179 else if (IsEnumerableType(propertyType))
180 {
181 int idx = 0;
182 foreach (var item in value)
183 {
184 var elementType = item.GetType();
185
186 if (true == IsSimpleType(elementType))
187 list.Add(item);
188 else if (true == IsComplexType(elementType))
189 {
190 string sign = item.Imaginary >= 0 ? "+" : "";
191 list.Add($"{item.Real.ToString(CultureInfo.InvariantCulture)}{sign}{item.Imaginary.ToString(CultureInfo.InvariantCulture)}i");
192 }
193 else
194 {
195 myList = ConvertClassToList(item);
196 list.Add(myList);
197 }
198 idx++;
199 }
200 }
201 else if (true == IsComplexType(propertyType))
202 {
203 string sign = value.Imaginary >= 0 ? "+" : "";
204 list.Add($"{value.Real.ToString(CultureInfo.InvariantCulture)}{sign}{value.Imaginary.ToString(CultureInfo.InvariantCulture)}i");
205 }
206 else
207 {
208 list.Add(ConvertClassToList(value));
209 }
210 }
211 }
212 }
213 }
214
215 if (null != fields)
216 foreach (FieldInfo field in fields)
217 {
218 {
219 object value = field.GetValue(myClass);
220 if (value != null)
221 {
222 if (IsSimpleType(field.FieldType))
223 {
224 list.Add(value);
225 }
226 else if (IsDictionaryType(field.FieldType))
227 {
228 list.Add(ConvertClassToList(value));
229 }
230 else if (IsEnumerableType(field.FieldType))
231 {
232 foreach (object item in (IEnumerable<dynamic>)value)
233 {
234 myList = ConvertClassToList(item);
235 list.Add(myList);
236 }
237 list.Add(list);
238 }
239 else
240 {
241 list.Add(ConvertClassToList(value));
242 }
243 }
244 }
245 }
246 return list;
247 }
248 // For correct reference equality in HashSet<object>
249 public class ReferenceEqualityComparer : IEqualityComparer<object>
250 {
251 public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer();
252
253 public new bool Equals(object x, object y) => ReferenceEquals(x, y);
254 public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
255 }
256 public static Dictionary<object, dynamic> ConvertClassToDictionary(
257 dynamic myClass,
258 string name = "",
259 HashSet<object> visited = null) // New parameter: tracks visited objects
260 {
261 // Initialize visited set at root call
262 bool isRootCall = visited == null;
263 if (isRootCall)
264 {
265 visited = new HashSet<object>(ReferenceEqualityComparer.Instance); // Critical: reference equality
266 }
267
268 Dictionary<object, dynamic> dict = new Dictionary<object, dynamic>();
269
270 if (false == IsCustomStructType(myClass?.GetType()) && false == IsTupleType(myClass?.GetType()) && myClass == null)
271 {
272 return dict;
273 }
274
275 // === CYCLE DETECTION: If this is a reference type we've already seen, return a marker ===
276 if (myClass.GetType().IsClass || myClass.GetType().IsInterface) // Reference types only
277 {
278 if (visited.TryGetValue(myClass, out dynamic fuggetAboutIt))
279 {
280 string referenceName = string.Empty;
281
282 try
283 {
284 referenceName = myClass.Value;
285 }
286 catch
287 {
288 try
289 {
290 referenceName = myClass.Name;
291 }
292 catch
293 {
294 referenceName = myClass.GetType().Name;
295 }
296 }
297 // We've seen this exact object before > return a reference marker
298 return new Dictionary<object, dynamic>
299 {
300 { name, $"Recursion to [{referenceName}]" }
301 };
302 }
303
304 // First time seeing this object > add to visited; remove on exit so only the
305 // current call-stack path is tracked (shared references are not false-positive cycles)
306 visited.Add(myClass);
307 }
308
309 bool addedToVisited = false == IsCustomStructType(myClass?.GetType()) && false == IsTupleType(myClass?.GetType()) && myClass != null && (myClass?.GetType().IsClass || myClass?.GetType().IsInterface);
310 try
311 {
312
313 Type myClassType = myClass?.GetType();
314 var properties = (from p in myClassType?.GetProperties(BindingFlags.Public | BindingFlags.Instance)
315 group p by p.Name into g
316 select g.OrderByDescending(t => t.DeclaringType == myClassType).First());
317
318 FieldInfo[] fields = myClassType?.GetFields(BindingFlags.Public | BindingFlags.Instance);
319 List<dynamic> list = new List<dynamic>();
320 IDictionary myDict;
321
322 // Skip collection checks for user-defined classes...
323 if (IsClassType(myClassType) &&
324 !myClassType.IsAbstract &&
325 !myClassType.IsInterface &&
326 !myClassType.IsArray &&
327 !IsSimpleType(myClassType) &&
328 !IsEnumerableType(myClassType) &&
329 !IsDictionaryType(myClassType) &&
330 !myClassType.IsDefined(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false))
331 {
332 goto ProcessPropertiesAndFields;
333 }
334
335 // Handle dictionary types
336 if (IsDictionaryType(myClassType))
337 {
338 // A complex/class-typed key falls back to Object.ToString() (the type's full name for
339 // every instance), so every entry would collide on the same string past the first -
340 // Dictionary<object, dynamic> already accepts non-string keys, so keep the key object
341 // itself instead. A simple-typed key (string, int, enum, etc.) keeps its own type/value
342 // via ConvertValue rather than being coerced to a string, so e.g. an int key round-trips
343 // back as an int, not "2".
344 void AddEntry(dynamic entryKey, dynamic entryValue)
345 {
346 object outputKey = IsSimpleType(entryKey?.GetType()) ? (object)ConvertValue(entryKey, entryKey?.GetType()) : entryKey;
347 // A simple-typed value (string, int, etc.) has no properties/fields to flatten -
348 // recursing it through ConvertClassToDictionary would just return an empty dict,
349 // discarding the value entirely (same reasoning as the key handling above).
350 object outputValue = IsSimpleType(entryValue?.GetType())
351 ? (object)ConvertValue(entryValue, entryValue?.GetType())
352 : ConvertClassToDictionary(entryValue, entryKey?.ToString(), visited);
353 dict.Add(outputKey, outputValue);
354 }
355
356 try
357 {
358 var dictionary = (IDictionary)myClass;
359 foreach (var key in dictionary.Keys)
360 {
361 AddEntry(key, dictionary[key]);
362 }
363 }
364 catch
365 {
366 dict.Clear();
367 foreach (var item in (IEnumerable)myClass)
368 {
369 var itemType = item.GetType();
370 var keyProp = itemType.GetProperty("Key");
371 var valueProp = itemType.GetProperty("Value");
372 if (keyProp != null && valueProp != null)
373 {
374 AddEntry(keyProp.GetValue(item), valueProp.GetValue(item));
375 }
376 else
377 {
378 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unable to convert class [{itemType}] Key [{keyProp?.Name}]/Value [{valueProp?.Name}] to dictionary.", myClass, GPALObjectType.Other);
379 }
380 }
381 }
382 return dict;
383 }
384 else if (IsEnumerableType(myClassType))
385 {
386 int idx = 0;
387
388 foreach (dynamic item in (IEnumerable)myClass)
389 {
390 Type elementType = item?.GetType();
391 if (elementType == null) continue;
392
393 string keyName = $"GPALKEY{idx:D4}";
394
395 if (IsSimpleType(elementType))
396 {
397 dict.Add(keyName, ConvertValue(item, elementType));
398 }
399 else if (IsComplexType(elementType))
400 {
401 string sign = item.Imaginary >= 0 ? "+" : "";
402 dict.Add(keyName, $"{item.Real.ToString(CultureInfo.InvariantCulture)}{sign}{item.Imaginary.ToString(CultureInfo.InvariantCulture)}i");
403 }
404 else if (IsClassType(elementType) && !IsEnumerableType(elementType) && !IsDictionaryType(elementType) && !elementType.IsArray)
405 {
406 myDict = ConvertClassToDictionary(item, "", visited);
407 dict.Add(keyName, myDict);
408 }
409 else if (IsDictionaryType(elementType) || IsEnumerableType(elementType) || elementType.IsArray)
410 {
411 myDict = ConvertClassToDictionary(item, "", visited);
412 dict.Add(keyName, myDict);
413 }
414 else
415 {
416 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported element type [{elementType}] in enumerable [{myClassType}]", item, GPALObjectType.Other);
417 }
418 idx++;
419 }
420 return dict;
421 }
422
423 ProcessPropertiesAndFields:
424 if (properties != null)
425 {
426 foreach (PropertyInfo property in properties)
427 {
428 if (property.GetIndexParameters().Length > 0)
429 {
430 continue;
431 }
432
433 if (property.DeclaringType == myClassType &&
434 property.DeclaringType.Namespace != null &&
435 property.DeclaringType.Namespace.StartsWith("System.Collections"))
436 {
437 continue;
438 }
439
440 Type propertyType = property.PropertyType;
441 list = new List<dynamic>();
442
443 if (property.CanRead)
444 {
445 dynamic value = null;
446 try
447 {
448 value = property.GetValue(myClass);
449 }
450 catch (Exception ex)
451 {
452 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to getvalue of property [{property.Name}][{propertyType}]. Continuing and hoping for the best.", myClass, GPALObjectType.Other, ex);
453 }
454
455 if ((object)value == null)
456 continue;
457
458 if (false == IsTupleType(value?.GetType()))
459 {
460 if (propertyType.IsArray)
461 {
462 var arrayList = (Array)property.GetValue(myClass);
463 if (null != arrayList)
464 for (int idx = 0; idx < arrayList.Length; idx++)
465 {
466 var elementValue = arrayList.GetValue(idx);
467 var elementType = elementValue?.GetType() ?? typeof(object);
468
469
470 if (IsSimpleType(elementType))
471 {
472 list.Add(ConvertValue(elementValue, elementType));
473 }
474 else if (IsEnumerableType(elementType))
475 {
476 list.Add((IEnumerable)elementValue);
477 }
478 else if (IsDictionaryType(elementType))
479 {
480 list.Add(ConvertClassToDictionary(elementValue, $"{property.Name}{idx}", visited));
481 }
482 else if (IsCustomStructType(elementType) || IsClassType(elementType) || IsTupleType(elementType))
483 {
484 list.Add(ConvertClassToDictionary(elementValue, $"{property.Name}{idx}", visited));
485 }
486 }
487 else
488 list.Add(ValueToString(arrayList));
489
490 if (list.Count > 0)
491 dict.Add(property.Name, list);
492 }
493 else if (IsSimpleType(propertyType))
494 {
495 dict.Add(property.Name, ConvertValue(value, propertyType));
496 }
497 else if (IsDictionaryType(propertyType))
498 {
499 dict.Add(property.Name, ConvertClassToDictionary(value, $"{propertyType}", visited));
500 }
501 else if (IsEnumerableType(propertyType))
502 {
503 int idx = 0;
504 if (null != value)
505 foreach (var item in value)
506 {
507 var elementType = item?.GetType() ?? typeof(object);
508 if (true == IsCustomStructType(item?.GetType()))
509 {
510 list.Add(ConvertClassToDictionary(item, $"{property.Name}{idx}", visited));
511 }
512 //else if (item == null)
513 //{
514 // list.Add(null);
515 //}
516 else if (IsSimpleType(elementType))
517 {
518 list.Add(ConvertValue(item, elementType));
519 }
520 else if (IsComplexType(elementType))
521 {
522 string sign = item.Imaginary >= 0 ? "+" : "";
523 list.Add($"{item.Real.ToString(CultureInfo.InvariantCulture)}{sign}{item.Imaginary.ToString(CultureInfo.InvariantCulture)}i");
524 }
525 else if (true == IsGuidType(elementType))
526 {
527 list.Add(Guid.TryParse(item.ToString(), out Guid outGuid) ? outGuid : item);
528 }
529 else if (IsEnumerableType(elementType))
530 {
531 list.Add((IEnumerable)item);
532 }
533 else
534 {
535 list.Add(ConvertClassToDictionary(item, $"{property.Name}{idx}", visited));
536 }
537 idx++;
538 }
539 else
540 list.Add(ValueToString(value));
541
542 if (list.Count > 0)
543 dict.Add(property.Name, list);
544 }
545 else if (IsComplexType(propertyType))
546 {
547 string sign = value.Imaginary >= 0 ? "+" : "";
548 dict.Add(property.Name, $"{value.Real.ToString(CultureInfo.InvariantCulture)}{sign}{value.Imaginary.ToString(CultureInfo.InvariantCulture)}i");
549 }
550 else if (IsClassType(propertyType) || IsCustomStructType(value?.GetType()))
551 {
552 dict.Add(property.Name, ConvertClassToDictionary(value, property.Name, visited));
553 }
554 else
555 {
556 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unknown property type [{myClass.GetType()}][{propertyType}] detected. Adding [{value}] to dict.", myClass, GPALObjectType.Other);
557 dict.Add(property.Name, value);
558 }
559 }
560 else // IsTupleType: serialize tuple fields (Item1/Item2/...) as a keyed dict
561 {
562 dict.Add(property.Name, ConvertClassToDictionary(value, property.Name, visited));
563 }
564 }
565 }
566 }
567
568 if (fields != null)
569 {
570 foreach (FieldInfo field in fields)
571 {
572 object value = field.GetValue(myClass);
573 if (true == IsCustomStructType(value?.GetType()) || value != null)
574 {
575 if (IsSimpleType(field.FieldType))
576 {
577 dict.Add(field.Name, ConvertValue(value, field.FieldType));
578 }
579 else if (IsDictionaryType(field.FieldType))
580 {
581 dict.Add(field.Name, ConvertClassToDictionary(value, field.Name, visited));
582 }
583 else if (IsEnumerableType(field.FieldType))
584 {
585 int idx = 0;
586 list = new List<dynamic>();
587 foreach (object item in (IEnumerable)value)
588 {
589 Type itemType = item?.GetType() ?? typeof(object);
590 list.Add(IsSimpleType(itemType) ? ConvertValue(item, itemType) : ConvertClassToDictionary(item, $"{itemType}{idx}", visited));
591 idx++;
592 }
593 dict.Add(field.Name, list);
594 }
595 else
596 {
597 dict.Add(field.Name, ConvertClassToDictionary(value, field.Name, visited));
598 }
599 }
600 }
601 }
602
603 return dict;
604
605 } // end try
606 finally
607 {
608 if (addedToVisited)
609 visited.Remove(myClass);
610 }
611 }
612 public static dynamic ConvertToClass(dynamic dictList, Type targetType, dynamic parentDictionary, List<string> columnNames)
613 {
614
615 Type createType = /*ConverterSettings.OutputClassElementType ??*/ GetConcreteType(targetType);
616 dynamic myDictList = dictList;
617 int dictListLength = 0;
618
619 dictListLength = ObjectCopier.GetItemCount(null, dictList);
620
621 if ((IsEnumerableObject(myDictList) || IsArray(myDictList)) && !IsDictionaryType(myDictList.GetType()) && 1 == dictListLength)
622 {
623 dynamic dictList2 = dictList[0];
624 if (true == IsDictionaryType(dictList2.GetType()))
625 {
626 myDictList = dictList2;
627 // dictListLength was the outer single-element wrapper's count (always 1 here) - recompute
628 // it for the unwrapped inner dictionary, or the nestedDictCount comparison below always
629 // fails against the stale outer count and silently takes the wrong branch.
630 dictListLength = ObjectCopier.GetItemCount(null, myDictList);
631 }
632 }
633
634 if (true == IsDictionaryType(myDictList.GetType()))
635 {
636 int nestedDictCount = 0;
637
638 foreach (var item in myDictList)
639 {
640 if (true == IsDictionaryType(item?.Value?.GetType()))
641 {
642 nestedDictCount++;
643 }
644 }
645
646 // A GPALKEY-indexed row dict ("0000": {...}, "0001": {...}) with only ONE row looks
647 // structurally identical to a genuine single-key wrapper (XML root element, JSON "root"
648 // wrap) - both are "one dict with one nested-dict value". The 1 < dictListLength guard
649 // below was disambiguating by assuming a single entry must be a wrapper, which silently
650 // misroutes a real one-row list (e.g. a client with exactly one MatchedData record) into
651 // the "unwrap as a single collapsed object" branch instead of "a list with one row". A
652 // wrapper's key is an arbitrary name (e.g. "root"); a real row index is always numeric, so
653 // check that instead of just the count.
654 bool singleRowLooksIndexed = false;
655 if (1 == dictListLength && 1 == nestedDictCount)
656 {
657 singleRowLooksIndexed = true;
658 foreach (var soloRowItem in myDictList)
659 {
660 string soloRowKeyStr = soloRowItem.Key?.ToString();
661 if (false == int.TryParse(soloRowKeyStr, out int soloRowKeyDummy))
662 {
663 singleRowLooksIndexed = false;
664 break;
665 }
666 }
667 }
668 // only a dictionary or a collection target can be filled below, and result[0] at the end of
669 // this branch indexes an empty list for anything else. two or more class members written as
670 // mappings and no scalar member (e.g. a class holding two trees) read as rows by this count
671 // and threw, so a plain class goes to the single-object path below instead
672 if (nestedDictCount == dictListLength && (1 < dictListLength || singleRowLooksIndexed)
673 && (true == IsDictionaryType(createType) || true == IsEnumerableType(createType))) // all nested dictionaries, so iterate over them
674 {
675 Type listType = typeof(List<>).MakeGenericType(createType);
676 IList result = (IList)InstantiateOne(listType);
677 // all nested dictionaries represent a collection of rows
678 // createType may be a collection type itself (List<T>, Queue<T>, Stack<T>, IGPALGrid<T>, etc.)
679 // We must instantiate the actual container (createType), populate it with the rows, then add that container to result
680 if (true == IsDictionaryType(createType))
681 {
682 IDictionary tmpDict = (IDictionary)InstantiateOne(createType);
683 Type dictKeyType = createType.GetGenericArguments()[0];
684 foreach (var item in myDictList)
685 {
686 try
687 {
688 dynamic element = ConvertToClass(item.Value, ConverterSettings.OutputClassElementType, parentDictionary, columnNames);
689 object resolvedKey = IsSimpleType(dictKeyType)
690 ? ResolveSimpleDictionaryKey(item.Key, dictKeyType)
691 : (item.Key is IDictionary keyDict ? CreateObjectFromDictionary(keyDict, dictKeyType, parentDictionary, null) : item.Key);
692 tmpDict[resolvedKey] = element;
693 }
694 catch
695 {
696 tmpDict = ConvertToClass(item.Value, createType, parentDictionary, columnNames);
697 }
698 }
699 result.Add(tmpDict);
700 }
701 else if (true == IsEnumerableType(createType))
702 {
703 object container = InstantiateOne(createType, dictListLength);
704
705 int index = 0;
706
707 // ConverterSettings.OutputClassElementType is a single shared field set once for the
708 // outermost SaveTo target - it's only valid at the first level of recursion. Two levels
709 // deep (e.g. Dictionary<Client, List<MatchedData>>, populating each client's List<MatchedData>
710 // from its own rows), that field still holds the outer List<MatchedData> type instead of
711 // MatchedData, so every row got recursed as "convert this record into a List<MatchedData>"
712 // instead of "into a MatchedData". Compute the row type locally from createType instead.
713 Type rowElementType = createType.IsArray
714 ? createType.GetElementType()
715 : (createType.IsGenericType && createType.GetGenericArguments().Length > 0 ? createType.GetGenericArguments()[0] : ConverterSettings.OutputClassElementType);
716
717 foreach (var item in myDictList)
718 {
719 object element = ConvertToClass(item.Value, rowElementType, parentDictionary, columnNames);
720
721 // Use your existing robust AddToList to add the row to the container
722 // It handles Add, Enqueue, Push, array indexing, dictionary, etc.
723 AddToList(container, element, index);
724
725 index++;
726 }
727
728 // Now add the fully populated container as the single item to result
729 // result is always List<createType>, so .Add is safe
730 result.Add(container);
731 }
732
733 if (1 < result.Count)
734 return result;
735 else
736 return result[0];
737 }
738 else
739 {
740 // Unwrap single-key container dicts (XML root element, JSON "root" wrap).
741 // If the sole key has no matching property/field on the target type, step into its value.
742 dynamic unwrapped = myDictList;
743 if (myDictList.Count == 1)
744 {
745 foreach (var kvp in myDictList)
746 {
747 if (kvp.Value is IDictionary)
748 {
749 string key = kvp.Key?.ToString() ?? string.Empty;
750 bool isRealProperty =
751 createType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
752 .Any(p => string.Equals(p.Name, key, StringComparison.OrdinalIgnoreCase))
753 || createType.GetFields(BindingFlags.Public | BindingFlags.Instance)
754 .Any(f => string.Equals(f.Name, key, StringComparison.OrdinalIgnoreCase));
755 if (!isRealProperty)
756 unwrapped = kvp.Value;
757 }
758 break;
759 }
760 }
761 return CreateObjectFromDictionary(unwrapped, createType, parentDictionary, columnNames);
762 }
763 }
764 else if (IsEnumerableObject(myDictList) || IsArray(myDictList))
765 {
766 Type listType = typeof(List<>).MakeGenericType(createType);
767 IList result = (IList)InstantiateOne(listType);
768 dynamic createdType = InstantiateOne(createType);
769 int count = 0;
770
771 // It's a list or an array, so we handle its elements
772 foreach (var item in myDictList)
773 {
774 object element = CreateObjectFromDictionary(item, ConverterSettings.OutputClassElementType, parentDictionary, columnNames);
775 AddToList(createdType, element, count++);
776 }
777
778 result.Add(createdType);
779
780 if (1 < result.Count)
781 return result;
782 else
783 return result[0];
784 }
785 else
786 {
787 // It's a single dictionary, so we create an object
788 return CreateObjectFromDictionary(myDictList, createType, parentDictionary, columnNames);
789 }
790 }
791
792 static dynamic lastParentDictionary = null;
793 static List<string> lastErrorMessage = new List<string>();
794 static bool supressedMessage = false;
795 [System.ThreadStatic]
796 private static HashSet<object> _dictVisited;
805 internal static object CreateObjectFromDictionary(dynamic dictionary, Type targetType, dynamic parentDictionary, List<string> columnNames)
806 {
807 // Cycle detection: YAML aliases resolve to the same object reference; track input
808 // dictionaries/lists on the current call path and return a default when revisited.
809 bool isRoot = _dictVisited == null;
810 if (isRoot) _dictVisited = new HashSet<object>(ReferenceEqualityComparer.Instance);
811 bool addedToVisited = false;
812 if (dictionary != null && !(targetType?.IsValueType == true))
813 {
814 object dictRef = (object)dictionary;
815 if (!_dictVisited.Add(dictRef))
816 return InstantiateOne(targetType); // circular alias — return default
817 addedToVisited = true;
818 }
819
820 dynamic objOrList = null;
821 string kvpName = string.Empty;
822 string kvpPropertyOrFieldName = string.Empty;
823 dynamic kvpValue = null;
824 PropertyInfo kvpProperty = null;
825 FieldInfo kvpField = null;
826 Type kvpPropertyType = null;
827 Type priorKvpPropertyType = null; // hack to get around json/yaml collapsing 1 item arrays into a single object
828 Type kvpFieldType = null;
829 object itemobj = null;
830 int noPropertyOrField = 0;
831 int count = 0;
832 bool isEnumerable = IsEnumerableType(targetType);
833
834 // XML parsers store every child as a List; for non-enumerable, non-array, non-dict targets
835 // unwrap a single-item list to its inner dict so the property loop below can process it.
836 if (dictionary is IList cofdSingleList && cofdSingleList.Count == 1 && cofdSingleList[0] is IDictionary cofdInnerDict
837 && !isEnumerable && !targetType.IsArray && !IsDictionaryType(targetType))
838 {
839 dictionary = cofdInnerDict;
840 }
841
842 // Returns true if the parent container has changed (including becoming null or from null)
843 bool HasParentChanged(dynamic lastParent, dynamic currentParent)
844 {
845 // If current is null, always treat as a change
846 // (null is not a valid trackable container)
847 if (currentParent == null)
848 return true;
849
850 // If last was null but current is not, it's a change (we're entering a new container)
851 if (lastParent == null)
852 return true;
853
854 // Both non-null: if types differ, it's a different kind of container -> change
855 if (lastParent.GetType() != currentParent.GetType())
856 return true;
857
858 // Same type and both non-null: compare by reference
859 // Different instance means the container object itself changed
860 return !ReferenceEquals(lastParent, currentParent);
861 }
862
863 if (true == HasParentChanged(lastParentDictionary, parentDictionary))
864 {
865 lastParentDictionary = parentDictionary;
866 lastErrorMessage.Clear();
867 supressedMessage = false;
868 }
869
870 if (true == isEnumerable || targetType.IsArray)
871 count = ObjectCopier.GetItemCount(null, dictionary);
872
873 if (IsComplexType(targetType))
874 return ComplexFromDict(dictionary);
875
876 // When the target is object (e.g. Dictionary<string, object> value slot), preserve the
877 // input dict as-is. Trying to map its keys to System.Object properties produces spurious
878 // "No property/field" warnings and discards the data.
879 if (targetType == typeof(object) && dictionary is IDictionary)
880 return dictionary;
881
882 try
883 {
884 objOrList = ConverterHelper.InstantiateOne(targetType, count);
885 }
886 catch
887 {
888 Type listType = typeof(List<>).MakeGenericType(targetType);
889 objOrList = ActivatorHelper.CreateInstance(listType);
890 }
891
892 if (IsDictionaryType(targetType))
893 {
894 if (dictionary is IDictionary dictInput)
895 {
896 Type keyType = targetType.GetGenericArguments()[0];
897 Type valueType = targetType.GetGenericArguments()[1];
898 IDictionary dict = (IDictionary)objOrList; // Cast to IDictionary for indexing
899 foreach (dynamic kvp in dictInput)
900 {
901 object key = IsSimpleType(keyType)
902 ? ResolveSimpleDictionaryKey(kvp.Key, keyType)
903 : (kvp.Key is IDictionary keyDict ? CreateObjectFromDictionary(keyDict, keyType, dictInput, null) : kvp.Key);
904 object value = kvp.Value?.ToString().Trim('"') ?? string.Empty; // we now parse CSV differently, " are literal data we have to remove
905
906 try
907 {
908 // Check if kvp.Value is already of the target valueType
909 if (kvp.Value != null && valueType.IsAssignableFrom(kvp.Value.GetType()))
910 {
911 value = kvp.Value; // Use directly if already of correct type
912 }
913 // Handle dictionary input for class type
914 else if (kvp.Value is IDictionary && IsClassType(valueType) && valueType != typeof(string))
915 {
916 value = ConvertToClass(kvp.Value, valueType, dictInput, columnNames); // Map dictionary to class
917 }
918 // Handle dictionary input for dictionary type
919 else if (IsDictionaryType(valueType) && kvp.Value is IDictionary)
920 {
921 value = CreateObjectFromDictionary(kvp.Value, valueType, dictInput, columnNames); // Recursive dictionary conversion
922 }
923 // Handle simple types
924 else if (IsSimpleType(valueType) || IsNullableType(valueType))
925 {
926 value = ConvertValue(kvp.Value, valueType);
927 }
928 // Handle list/array values (e.g., Dictionary<string, List<T>>)
929 else if (IsEnumerableType(valueType) && valueType != typeof(string))
930 {
931 Type elemType = valueType.GetGenericArguments().Length > 0
932 ? valueType.GetGenericArguments()[0]
933 : valueType.GetElementType();
934 if (elemType != null)
935 {
936 dynamic resultList = InstantiateOne(valueType);
937 int i2 = 0;
938 IEnumerable innerEnum = kvp.Value is IEnumerable enumer && !(kvp.Value is string)
939 ? enumer
940 : new object[] { kvp.Value };
941 foreach (var innerItem in innerEnum)
942 {
943 if (innerItem is IDictionary innerDict && IsClassType(elemType) && elemType != typeof(string))
944 AddToList(resultList, CreateObjectFromDictionary(innerDict, elemType, dictInput, columnNames), i2++);
945 else if (IsSimpleType(elemType) || IsNullableType(elemType))
946 AddToList(resultList, ConvertValue(innerItem, elemType), i2++);
947 else if (IsBigIntegerType(elemType))
948 {
949 if (innerItem is string biNullStr2 && IsNullableType(elemType) && biNullStr2.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
950 AddToList(resultList, null, i2++);
951 else
952 {
953 try { AddToList(resultList, AsBigInteger(innerItem), i2++); }
954 catch { GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid BigInteger format [{innerItem}] for element type [{elemType}]", innerItem, GPALObjectType.Other); }
955 }
956 }
957 else
958 AddToList(resultList, innerItem, i2++);
959 }
960 value = resultList;
961 }
962 }
963
964 dict[key] = value; // Assign to dictionary
965 }
966 catch (Exception ex)
967 {
968 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to add key[{key}]-value[{value}] pair to dictionary [{targetType}]", kvp, GPALObjectType.Other, ex);
969 }
970 }
971 return dict;
972 }
973 else
974 {
975 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Expected IDictionary input for [{targetType}]", dictionary, GPALObjectType.None);
976 }
977 }
978
979 // New case for Queue<T>, Stack<T>, Hashtable, and other enumerables
980 else if (true == isEnumerable)
981 {
982 Type elementType = targetType.IsArray ? targetType.GetElementType() : (targetType.GetGenericArguments().Length > 0 ? targetType.GetGenericArguments()[0] : targetType);
983 if (dictionary is IDictionary dictInput)
984 {
985 bool isIndexed = dictInput.Keys.Cast<dynamic>().All(key => int.TryParse(key?.ToString(), out int dummy));
986 if (isIndexed)
987 {
988 dynamic keys = dictInput.Keys;
989
990 foreach (var key in keys)
991 {
992 try
993 {
994 object value = dictInput[key];
995 if (value is IDictionary classDict && IsClassType(elementType) && elementType != typeof(string))
996 {
997 object classInstance = CreateObjectFromDictionary(classDict, elementType, keys, columnNames);
998 if (classInstance != null)
999 {
1000 AddToList(objOrList, classInstance, count++);
1001 }
1002 else
1003 {
1004 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"ConvertToClass returned null for [{elementType}] with dict [{classDict}]", classDict, GPALObjectType.Other);
1005 }
1006 }
1007 else if (IsSimpleType(elementType) && (value == null || IsSimpleType(value.GetType())))
1008 {
1009 // Handle simple types directly
1010 object convertedValue = ConvertValue(value, elementType);
1011 AddToList(objOrList, convertedValue, count++);
1012 }
1013 else
1014 {
1015 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Expected IDictionary for TestClass or simple type, got [{value?.GetType()}] for element type [{elementType}]", key, GPALObjectType.Other);
1016 }
1017 }
1018 catch (Exception ex)
1019 {
1020 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to add value to enumerable [{targetType}]", key, GPALObjectType.Other, ex);
1021 }
1022 }
1023 }
1024 else
1025 {
1026 foreach (dynamic kvp in dictionary)
1027 {
1028 {
1029 try
1030 {
1031 dynamic value = kvp.Value;
1032 if (value == null)
1033 {
1034 if (elementType.IsValueType && !IsNullableType(elementType))
1035 {
1036 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Null value not allowed for non-nullable type [{elementType}]", kvp, GPALObjectType.Other);
1037 }
1038 else
1039 {
1040 AddToList(objOrList, null, count++);
1041 }
1042 }
1043 else if (elementType.IsAssignableFrom(value.GetType()))
1044 {
1045 AddToList(objOrList, value, count++);
1046 }
1047 else if (IsCustomStructType(elementType))
1048 {
1049 // Handle structs not covered by other checks
1050 var converter = TypeDescriptor.GetConverter(elementType);
1051 if (converter != null && converter.CanConvertFrom(value.GetType()))
1052 {
1053 value = converter.ConvertFrom(value);
1054 AddToList(objOrList, value, count++);
1055 }
1056 else
1057 {
1058 try
1059 {
1060 AddToList(objOrList, value, count++);
1061 }
1062 catch
1063 {
1064 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported struct type [{value.GetType()}] for element type [{elementType}]", kvp, GPALObjectType.Other);
1065 }
1066 }
1067 }
1068 else if (value is IDictionary nestedDict)
1069 {
1070 if (IsClassType(elementType) && elementType != typeof(string))
1071 {
1072 bool nestedIsIndexed = nestedDict.Keys.Cast<dynamic>().All(k => int.TryParse(k?.ToString(), out int dummy));
1073 if (!nestedIsIndexed)
1074 {
1075 value = ConvertToClass(nestedDict, elementType, dictionary, columnNames);
1076 AddToList(objOrList, value, count++);
1077 }
1078 }
1079 else if (IsDictionaryType(elementType) || IsCustomStructType(elementType) || IsTupleType(elementType))
1080 {
1081 value = CreateObjectFromDictionary(nestedDict, elementType, dictionary, columnNames);
1082 AddToList(objOrList, value, count++);
1083 }
1084 else
1085 {
1086 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Dictionary value [{value.GetType()}] incompatible with element type [{elementType}]", kvp, GPALObjectType.Other);
1087 }
1088 }
1089 else if (IsEnumerableType(elementType) && value is IEnumerable enumValue)
1090 {
1091 value = CreateObjectFromDictionary(enumValue, elementType, dictionary, columnNames);
1092 AddToList(objOrList, value, count++);
1093 }
1094 else if (IsSimpleType(elementType) || IsNullableType(elementType))
1095 {
1096 if (IsSimpleType(value.GetType()) || IsNullableType(value.GetType()))
1097 {
1098 value = ConvertValue(value, elementType);
1099 AddToList(objOrList, value, count++);
1100 }
1101 else
1102 {
1103 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Value type [{value.GetType()}] incompatible with simple/nullable element type [{elementType}]", kvp, GPALObjectType.Other);
1104 }
1105 }
1106 else if (IsComplexType(elementType))
1107 {
1108 if (value is string complexStr)
1109 {
1110 value = ParseComplex(complexStr);
1111 AddToList(objOrList, value, count++);
1112 }
1113 else if (value is Complex complexValue)
1114 {
1115 AddToList(objOrList, complexValue, count++);
1116 }
1117 else
1118 {
1119 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Value type [{value.GetType()}] incompatible with complex/nullable element type [{elementType}]", kvp, GPALObjectType.Other);
1120 }
1121 }
1122 else if (IsBigIntegerType(elementType))
1123 {
1124 if (value is string biNullStr && IsNullableType(elementType) && biNullStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1125 AddToList(objOrList, null, count++);
1126 else
1127 {
1128 try { AddToList(objOrList, AsBigInteger(value), count++); }
1129 catch { GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid BigInteger format [{value}] for element type [{elementType}]", kvp, GPALObjectType.Other); }
1130 }
1131 }
1132 else if (IsVersionType(elementType))
1133 {
1134 if (value is string versionStr && System.Version.TryParse(versionStr, out System.Version version))
1135 {
1136 AddToList(objOrList, version, count++);
1137 }
1138 else
1139 {
1140 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Version format [{value}] for element type [{elementType}]", kvp, GPALObjectType.Other);
1141 }
1142 }
1143 else if (true == IsIpAddressType(elementType))
1144 {
1145 if (value is string ipStr && IPAddress.TryParse(ipStr, out IPAddress ipAddress))
1146 {
1147 AddToList(objOrList, ipAddress, count++);
1148 }
1149 else
1150 {
1151 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid IPAddress format [{value}] for element type [{elementType}]", kvp, GPALObjectType.Other);
1152 }
1153 }
1154 else if (true == IsUriType(elementType))
1155 {
1156 if (value is string uriStr && Uri.TryCreate(uriStr, UriKind.Absolute, out Uri uri))
1157 {
1158 AddToList(objOrList, uri, count++);
1159 }
1160 else
1161 {
1162 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Uri format [{value}] for element type [{elementType}]", kvp, GPALObjectType.Other);
1163 }
1164 }
1165 else if (true == IsGuidType(elementType))
1166 {
1167 if (value is string guidStr && IsNullableType(elementType) && guidStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1168 AddToList(objOrList, null, count++);
1169 else if (value is string guidStr2 && Guid.TryParse(guidStr2, out Guid guid))
1170 AddToList(objOrList, guid, count++);
1171 else
1172 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Guid format [{value}] for element type [{elementType}]", kvp, GPALObjectType.Other);
1173 }
1174 else if (true == IsTimeSpanType(elementType))
1175 {
1176 if (value is string timeSpanStr && IsNullableType(elementType) && timeSpanStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1177 AddToList(objOrList, null, count++);
1178 else if (value is string timeSpanStr2 && TimeSpan.TryParse(timeSpanStr2, out TimeSpan timeSpan))
1179 AddToList(objOrList, timeSpan, count++);
1180 else
1181 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid TimeSpan format [{value}] for element type [{elementType}]", kvp, GPALObjectType.Other);
1182 }
1183 else if (IsClassType(elementType) && elementType != typeof(string))
1184 {
1185 // Handle non-dictionary class types (e.g., custom structs or classes)
1186 var converter = TypeDescriptor.GetConverter(elementType);
1187 if (converter != null && converter.CanConvertFrom(value.GetType()))
1188 {
1189 value = converter.ConvertFrom(value);
1190 AddToList(objOrList, value, count++);
1191 }
1192 else
1193 {
1194 AddToList(objOrList, value, count++);
1195 //GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported class type [{value.GetType()}] for element type [{elementType}]", kvp, GPALObjectType.Other);
1196 }
1197 }
1198 else
1199 {
1200 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported value type [{value.GetType()}] for element type [{elementType}]", kvp, GPALObjectType.Other);
1201 }
1202 }
1203 catch (Exception ex)
1204 {
1205 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to add value to enumerable [{targetType}]", kvp, GPALObjectType.Other, ex);
1206 }
1207 }
1208 }
1209 }
1210 }
1211 else if (dictionary is IEnumerable && !IsDictionaryType(dictionary.GetType()))
1212 {
1213 IEnumerable enumInput = (IEnumerable)dictionary;
1214 foreach (dynamic item in enumInput)
1215 {
1216 try
1217 {
1218 dynamic value = item;
1219 if (value == null)
1220 {
1221 if (elementType.IsValueType && !IsNullableType(elementType))
1222 {
1223 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Null value not allowed for non-nullable type [{elementType}]", item, GPALObjectType.Other);
1224 }
1225 else
1226 {
1227 AddToList(objOrList, null, count++);
1228 }
1229 }
1230 else if (elementType.IsAssignableFrom(value.GetType()))
1231 {
1232 AddToList(objOrList, value, count++);
1233 }
1234 else if (elementType.IsArray)
1235 {
1236 var arrayElementType = elementType.GetElementType();
1237 var listType = typeof(List<>).MakeGenericType(arrayElementType);
1238 var tempList = (IList)InstantiateOne(listType);
1239 if (value is IEnumerable arrayValues)
1240 {
1241 foreach (var arrayItem in arrayValues)
1242 {
1243 var convertedItem = CreateObjectFromDictionary(arrayItem, arrayElementType, enumInput, columnNames);
1244 tempList.Add(convertedItem);
1245 }
1246 }
1247 value = tempList.Cast<object>().ToArray();
1248 AddToList(objOrList, value, count++);
1249 }
1250 else if (IsEnumerableType(elementType) && value is IEnumerable enumValue)
1251 {
1252 value = CreateObjectFromDictionary(enumValue, elementType, enumInput, columnNames);
1253 AddToList(objOrList, value, count++);
1254 }
1255 else if (IsComplexType(elementType))
1256 {
1257 if (value is string complexStr)
1258 {
1259 value = ParseComplex(complexStr);
1260 AddToList(objOrList, value, count++);
1261 }
1262 else if (value is Complex complexValue)
1263 {
1264 AddToList(objOrList, complexValue, count++);
1265 }
1266 else if (value is IDictionary || (value is IEnumerable && !(value is string)))
1267 {
1268 AddToList(objOrList, ComplexFromDict(value), count++);
1269 }
1270 else
1271 {
1272 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Value type [{value.GetType()}] incompatible with complex/nullable element type [{elementType}]", value, GPALObjectType.Other);
1273 }
1274 }
1275 else if (IsBigIntegerType(elementType))
1276 {
1277 if (value is string biNullStr4 && IsNullableType(elementType) && biNullStr4.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1278 AddToList(objOrList, null, count++);
1279 else
1280 {
1281 try { AddToList(objOrList, AsBigInteger(value), count++); }
1282 catch { GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid BigInteger format [{value}] for element type [{elementType}]", item, GPALObjectType.Other); }
1283 }
1284 }
1285 else if (IsVersionType(elementType))
1286 {
1287 if (value is string versionStr && System.Version.TryParse(versionStr, out System.Version version))
1288 {
1289 AddToList(objOrList, version, count++);
1290 }
1291 else
1292 {
1293 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Version format [{value}] for element type [{elementType}]", item, GPALObjectType.Other);
1294 }
1295 }
1296 else if (true == IsIpAddressType(elementType))
1297 {
1298 if (value is string ipStr && IPAddress.TryParse(ipStr, out IPAddress ipAddress))
1299 {
1300 AddToList(objOrList, ipAddress, count++);
1301 }
1302 else
1303 {
1304 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid IPAddress format [{value}] for element type [{elementType}]", item, GPALObjectType.Other);
1305 }
1306 }
1307 else if (true == IsUriType(elementType))
1308 {
1309 if (value is string uriStr && Uri.TryCreate(uriStr, UriKind.Absolute, out Uri uri))
1310 {
1311 AddToList(objOrList, uri, count++);
1312 }
1313 else
1314 {
1315 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Uri format [{value}] for element type [{elementType}]", item, GPALObjectType.Other);
1316 }
1317 }
1318 else if (true == IsGuidType(elementType))
1319 {
1320 if (value is string guidStr && IsNullableType(elementType) && guidStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1321 AddToList(objOrList, null, count++);
1322 else if (value is string guidStr2 && Guid.TryParse(guidStr2, out Guid guid))
1323 AddToList(objOrList, guid, count++);
1324 else
1325 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Guid format [{value}] for element type [{elementType}]", item, GPALObjectType.Other);
1326 }
1327 else if (true == IsTimeSpanType(elementType))
1328 {
1329 if (value is string timeSpanStr && IsNullableType(elementType) && timeSpanStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1330 AddToList(objOrList, null, count++);
1331 else if (value is string timeSpanStr2 && TimeSpan.TryParse(timeSpanStr2, out TimeSpan timeSpan))
1332 AddToList(objOrList, timeSpan, count++);
1333 else
1334 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid TimeSpan format [{value}] for element type [{elementType}]", item, GPALObjectType.Other);
1335 }
1336 else if (true == IsBooleanType(elementType))
1337 {
1338 if (value is string boolStr)
1339 {
1340 if (IsNullableType(elementType) && boolStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1341 {
1342 AddToList(objOrList, null, count++);
1343 }
1344 else if (bool.TryParse(boolStr.Trim(), out bool parsedBool))
1345 {
1346 AddToList(objOrList, parsedBool, count++);
1347 }
1348 else // if we are mapping from a file to a class, then the file might be trying to map out these values as boolean
1349 {
1350 // Common aliases that bool.TryParse doesn't accept by default
1351 string lower = boolStr.Trim().ToLowerInvariant();
1352 if (lower == "1" || lower == "yes" || lower == "on" || lower == "true")
1353 {
1354 AddToList(objOrList, true, count++);
1355 }
1356 else if (lower == "0" || lower == "no" || lower == "off" || lower == "false")
1357 {
1358 AddToList(objOrList, false, count++);
1359 }
1360 else
1361 {
1362 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
1363 $"Invalid boolean format [{value}] for element type [{elementType}]",
1364 item, GPALObjectType.Other);
1365 }
1366 }
1367 }
1368 else if (value is bool directBool)
1369 {
1370 AddToList(objOrList, directBool, count++);
1371 }
1372 else
1373 {
1374 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
1375 $"Invalid type for boolean: expected string or bool, got [{value?.GetType()}]",
1376 item, GPALObjectType.Other);
1377 }
1378 }
1379 else if (IsSimpleType(elementType) || IsNullableType(elementType))
1380 {
1381 if (IsSimpleType(value.GetType()) || IsNullableType(value.GetType()))
1382 {
1383 value = ConvertValue(value, elementType);
1384 AddToList(objOrList, value, count++);
1385 }
1386 else
1387 {
1388 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Value type [{value.GetType()}] incompatible with simple/nullable element type [{elementType}]", item, GPALObjectType.Other);
1389 }
1390 }
1391 else if (IsClassType(elementType) && elementType != typeof(string))
1392 {
1393 var converter = TypeDescriptor.GetConverter(elementType);
1394 if (converter != null && converter.CanConvertFrom(value.GetType()))
1395 {
1396 value = converter.ConvertFrom(value);
1397 AddToList(objOrList, value, count++);
1398 }
1399 else
1400 {
1401 try
1402 {
1403 AddToList(objOrList, value, count++);
1404 }
1405 catch
1406 {
1407 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported class type [{value.GetType()}] for element type [{elementType}]", item, GPALObjectType.Other);
1408 }
1409 }
1410 }
1411 else if (IsCustomStructType(elementType))
1412 {
1413 // Unwrap single-item XML list wrapper if needed
1414 if (value is IList svl2 && svl2.Count == 1 && svl2[0] is IDictionary svd2)
1415 value = svd2;
1416 if (value is IDictionary svStructDict2)
1417 {
1418 value = CreateObjectFromDictionary(svStructDict2, elementType, enumInput, columnNames);
1419 AddToList(objOrList, value, count++);
1420 }
1421 else
1422 {
1423 var converter = TypeDescriptor.GetConverter(elementType);
1424 if (converter != null && converter.CanConvertFrom(value.GetType()))
1425 {
1426 value = converter.ConvertFrom(value);
1427 AddToList(objOrList, value, count++);
1428 }
1429 else if (false == IsSimpleType(value.GetType()))
1430 {
1431 try
1432 {
1433 AddToList(objOrList, value, count++);
1434 }
1435 catch
1436 {
1437 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported struct type [{value.GetType()}] for element type [{elementType}]", item, GPALObjectType.Other);
1438 }
1439 }
1440 }
1441 }
1442 else if (value is IDictionary nestedDict)
1443 {
1444 if (IsClassType(elementType) && elementType != typeof(string))
1445 {
1446 value = ConvertToClass(nestedDict, elementType, enumInput, columnNames);
1447 AddToList(objOrList, value, count++);
1448 }
1449 else if (IsDictionaryType(elementType) || IsTupleType(elementType))
1450 {
1451 value = CreateObjectFromDictionary(nestedDict, elementType, enumInput, columnNames);
1452 AddToList(objOrList, value, count++);
1453 }
1454 else
1455 {
1456 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Dictionary value [{value.GetType()}] incompatible with element type [{elementType}]", item, GPALObjectType.Other);
1457 }
1458 }
1459 else
1460 {
1461 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported item type [{value?.GetType()}] for element type [{elementType}]", item, GPALObjectType.Other);
1462 }
1463 }
1464 catch (Exception ex)
1465 {
1466 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to add item to enumerable [{targetType}]", item, GPALObjectType.Other, ex);
1467 }
1468 }
1469 }
1470 }
1471 else if (!(dictionary is IDictionary inputDict)) // if we can't cast to dict for next step, we have something weird...
1472 {
1473 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Expected IDictionary input for [{targetType}]", dictionary, GPALObjectType.None);
1474 }
1475 else
1476 {
1477 int columnIdx = 0;
1478 foreach (dynamic kvp in inputDict)
1479 {
1480 noPropertyOrField = 0;
1481 priorKvpPropertyType = kvpPropertyType;
1482
1483 if (IsKeyValuePairType(kvp.GetType()))
1484 {
1485 if (null != columnNames && columnIdx < columnNames.Count)
1486 kvpName = columnNames[columnIdx++].Trim('"');
1487 else
1488 {
1489 string rawKey = kvp.Key?.ToString().Trim('"') ?? string.Empty;
1490 if (rawKey.StartsWith("GPALKEY", StringComparison.Ordinal))
1491 rawKey = rawKey.Substring(7);
1492 else if (System.Text.RegularExpressions.Regex.IsMatch(rawKey, @"^GPAL\d+_"))
1493 rawKey = System.Text.RegularExpressions.Regex.Replace(rawKey, @"^GPAL\d+_", "");
1494 kvpName = rawKey;
1495 }
1496
1497 if (typeof(string) == kvp.Value?.GetType())
1498 kvpValue = kvp.Value?.ToString().Trim('"');
1499 else
1500 kvpValue = kvp.Value;
1501
1502 kvpProperty = targetType.GetProperty(kvpName)
1503 ?? targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
1504 .FirstOrDefault(p => p.Name.Equals(kvpName, StringComparison.OrdinalIgnoreCase));
1505
1506 kvpField = targetType.GetField(kvpName)
1507 ?? targetType.GetFields(BindingFlags.Public | BindingFlags.Instance)
1508 .FirstOrDefault(f => f.Name.Equals(kvpName, StringComparison.OrdinalIgnoreCase));
1509
1510 if (kvpProperty != null)
1511 {
1512 kvpPropertyType = kvpProperty.PropertyType;
1513 kvpPropertyOrFieldName = kvpProperty.Name;
1514 }
1515 if (kvpField != null)
1516 {
1517 kvpFieldType = kvpField.FieldType;
1518 kvpPropertyOrFieldName = kvpField.Name;
1519 }
1520 }
1521 else
1522 {
1523 kvpValue = kvp;
1524 }
1525
1526 if (kvpProperty == null && kvpField == null)
1527 {
1528 kvpPropertyType = targetType;
1529 noPropertyOrField = 2;
1530 }
1531
1532 if (kvpProperty != null && kvpProperty.CanWrite && kvpProperty.Name.Equals(kvpName, StringComparison.OrdinalIgnoreCase))
1533 {
1534 if (IsDictionaryType(kvpPropertyType))
1535 {
1536 if (kvpValue is IList kvpSingleWrap && kvpSingleWrap.Count == 1 && kvpSingleWrap[0] is IDictionary kvpUnwrappedInner)
1537 kvpValue = kvpUnwrappedInner;
1538 if (kvpValue == null || (kvpValue is string kvpDictNullStr && kvpDictNullStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase)))
1539 {
1540 // null (C# null or XML "null" string) — leave dict property at default null
1541 }
1542 else if (kvpValue is IDictionary valueDict)
1543 {
1544 IDictionary propDict = (IDictionary)ConverterHelper.InstantiateOne(kvpPropertyType);
1545 Type keyType = kvpPropertyType.GetGenericArguments()[0];
1546 Type valueType = kvpPropertyType.GetGenericArguments()[1];
1547
1548 foreach (dynamic valueKvp in valueDict)
1549 {
1550 try
1551 {
1552 object key = IsSimpleType(keyType)
1553 ? ResolveSimpleDictionaryKey(valueKvp.Key, keyType)
1554 : (valueKvp.Key is IDictionary valueKeyDict ? CreateObjectFromDictionary(valueKeyDict, keyType, valueDict, null) : valueKvp.Key);
1555 object value;
1556
1557 if (IsKeyValuePairType(valueKvp.Value?.GetType()))
1558 {
1559 var nestedKvp = valueKvp.Value;
1560 value = IsDictionaryType(valueType)
1561 ? CreateObjectFromDictionary(new Dictionary<object, object> { { nestedKvp.Key, nestedKvp.Value } }, valueType, valueDict, columnNames)
1562 : ConvertValue(nestedKvp.Value, valueType, null, kvpName);
1563 }
1564 else if (IsDictionaryType(valueKvp.Value?.GetType()))
1565 {
1566 // If target is object or already assignable, preserve the dict as-is.
1567 // CreateObjectFromDictionary(dict, object) would try to map keys to
1568 // System.Object properties and fail.
1569 if (valueType == typeof(object) || valueType.IsAssignableFrom(valueKvp.Value.GetType()))
1570 value = valueKvp.Value;
1571 else
1572 value = CreateObjectFromDictionary(valueKvp.Value, valueType, valueDict, columnNames);
1573 }
1574 else
1575 {
1576 value = ConvertValue(valueKvp.Value, valueType, null, kvpName);
1577 }
1578
1579 if (value == null || valueType.IsAssignableFrom(value.GetType()))
1580 propDict[key] = value;
1581 else
1582 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to convert [{DescribeForLog(value)}] to [{valueType.Name}] for [{kvpName}]", valueKvp, GPALObjectType.None);
1583 }
1584 catch (Exception ex)
1585 {
1586 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to process key-value pair for [{kvpName}]", valueKvp, GPALObjectType.None, ex);
1587 }
1588 }
1589 if (objOrList.GetType().IsValueType)
1590 {
1591 string bfName = $"<{kvpProperty.Name}>k__BackingField";
1592 objOrList = GetSetStructMethod(objOrList.GetType()).Invoke(null, new object[] { (object)objOrList, bfName, propDict });
1593 }
1594 else
1595 kvpProperty.SetValue(objOrList, propDict);
1596 }
1597 else
1598 {
1599 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Value for property [{kvpName}] is not a dictionary", kvpValue, GPALObjectType.None);
1600 }
1601 }
1602 else if (IsEnumerableType(kvpPropertyType) || kvpPropertyType.IsArray)
1603 {
1604 Type elementType = kvpPropertyType.IsArray ? kvpPropertyType.GetElementType() : kvpPropertyType.GetGenericArguments()[0];
1605 itemobj = ConverterHelper.InstantiateOne(elementType);
1606
1607 count = 0;
1608 if (kvpValue is string kvpNullCollStr && kvpNullCollStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1609 {
1610 // "null" string from XML means the collection itself is null — leave property at default.
1611 }
1612 else if (kvpValue is IDictionary collapsedDict && !IsDictionaryType(elementType) && (IsClassType(elementType) || IsCustomStructType(elementType) || IsTupleType(elementType)))
1613 {
1614 // A JS array-like object (e.g. DOMRectList from getClientRects()) serializes to
1615 // {"0": {...}, "1": {...}} instead of a JSON array - that's a container of
1616 // elements keyed by index, not a single collapsed element like the "YAML/JSON
1617 // collapsed a single-element array to a plain mapping" case below it is meant for.
1618 bool looksLikeIndexedArray = collapsedDict.Keys.Cast<object>().All(k => int.TryParse(k?.ToString(), out _));
1619 object innerList = ConverterHelper.InstantiateOne(kvpPropertyType, looksLikeIndexedArray ? collapsedDict.Count : 1);
1620 if (looksLikeIndexedArray)
1621 {
1622 int indexedIdx = 0;
1623 foreach (var indexedKey in collapsedDict.Keys)
1624 {
1625 object indexedElement = collapsedDict[indexedKey] is IDictionary indexedElementDict
1626 ? CreateObjectFromDictionary(indexedElementDict, elementType, parentDictionary, columnNames)
1627 : ConvertValue(collapsedDict[indexedKey], elementType);
1628 AddToList(innerList, indexedElement, indexedIdx++);
1629 }
1630 }
1631 else
1632 {
1633 object element = CreateObjectFromDictionary(collapsedDict, elementType, parentDictionary, columnNames);
1634 AddToList(innerList, element, 0);
1635 }
1636 if (ObjectCopier.GetItemCount(kvpProperty, innerList) > 0)
1637 {
1638 if (IsEnumerableObject(objOrList) || objOrList.GetType().IsArray)
1639 AddToList(objOrList, innerList, count++);
1640 else if (objOrList.GetType().IsValueType)
1641 {
1642 string bfName = $"<{kvpProperty.Name}>k__BackingField";
1643 objOrList = GetSetStructMethod(objOrList.GetType()).Invoke(null, new object[] { (object)objOrList, bfName, innerList });
1644 }
1645 else
1646 kvpProperty.SetValue(objOrList, innerList);
1647 }
1648 }
1649 else if (IsEnumerableObject(kvpValue) || IsArray(kvpValue))
1650 {
1651 // yaml and json have a nasty habit of collapsing arrays with one elemnt to a singular type, messes us up when we expect arrays not a singular value
1652 Type innerElementType = null;
1653
1654 try
1655 {
1656 innerElementType = kvpValue.IsArray ? kvpValue.GetElementType() : kvpValue.GetGenericArguments()[0];
1657 }
1658 catch
1659 {
1660 try
1661 {
1662 // For tuple element types their first generic arg is NOT the collection element
1663 // type (it's just Item1's type), so leave innerElementType null.
1664 innerElementType = IsTupleType(elementType) ? null : elementType.GetGenericArguments()[0];
1665 }
1666 catch
1667 {
1668 innerElementType = elementType;
1669 }
1670 }
1671
1672 object innerList = ConverterHelper.InstantiateOne(kvpPropertyType, ObjectCopier.GetItemCount(kvpProperty, kvpValue));
1673 foreach (dynamic kvpValueItem in (IEnumerable)kvpValue)
1674 {
1675 Type kvpValueItemType = kvpValueItem.GetType();
1676 object element = null;
1677
1678 if (false == IsKeyValuePairType(kvpValueItemType) && false == IsCustomStructType(kvpValueItemType) && kvpValueItem == null)
1679 {
1680 if (elementType.IsValueType && !IsNullableType(elementType))
1681 {
1682 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Null value not allowed for non-nullable type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1683 }
1684 else
1685 {
1686 AddToList(innerList, null, count++);
1687 }
1688 }
1689 else if (elementType.IsAssignableFrom(kvpValueItem.GetType()))
1690 {
1691 AddToList(innerList, kvpValueItem, count++);
1692 }
1693 else if (kvpValueItem is IDictionary nestedDict)
1694 {
1695 if ((IsCustomStructType(elementType) || IsClassType(elementType)) && elementType != typeof(string))
1696 {
1697 element = ConvertToClass(nestedDict, elementType, parentDictionary, columnNames);
1698 AddToList(innerList, element, count++);
1699 }
1700 else if (IsDictionaryType(kvpValueItem.GetType()) || IsDictionaryType(elementType) || true == IsEnumerableType(elementType))
1701 {
1702 element = CreateObjectFromDictionary(nestedDict, elementType, parentDictionary, columnNames);
1703 AddToList(innerList, element, count++);
1704 }
1705 else
1706 {
1707 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Dictionary value [{kvpValueItem.GetType()}] incompatible with element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1708 }
1709 }
1710 else if (IsKeyValuePairType(kvpValueItemType))
1711 {
1712 var nestedKvp = kvpValueItem;
1713 Type valueType = elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(Dictionary<,>)
1714 ? elementType.GetGenericArguments()[1]
1715 : elementType;
1716
1717 if (IsDictionaryType(elementType))
1718 {
1719 element = CreateObjectFromDictionary(new Dictionary<object, object> { { nestedKvp.Key, nestedKvp.Value } }, elementType, parentDictionary, columnNames);
1720 }
1721 else if (IsComplexType(valueType))
1722 {
1723 element = ParseComplex(nestedKvp.Value?.ToString());
1724 }
1725 else if (IsSimpleType(valueType))
1726 {
1727 element = ConvertValue(nestedKvp.Value?.ToString(), valueType, null, kvpName);
1728 }
1729 else
1730 {
1731 try
1732 {
1733 element = CreateObjectFromDictionary(nestedKvp.Value, elementType, parentDictionary, columnNames);
1734 }
1735 catch
1736 {
1737 element = nestedKvp.Value;
1738 }
1739 }
1740 AddToList(innerList, element, count++);
1741 }
1742 else if (IsTupleType(elementType) && kvpValueItem is IList tupleBundledList)
1743 {
1744 // The whole tuple sequence arrived as a single List<object> item.
1745 // Unpack each inner dict as one tuple element.
1746 foreach (dynamic tupleSubItem in tupleBundledList)
1747 {
1748 if (tupleSubItem is IDictionary tupleSubDict)
1749 {
1750 element = CreateObjectFromDictionary(tupleSubDict, elementType, parentDictionary, columnNames);
1751 AddToList(innerList, element, count++);
1752 }
1753 }
1754 }
1755 else if (IsComplexType(elementType) || (true == IsComplexType(innerElementType) && typeof(string) == kvpValueItemType))
1756 {
1757 if (kvpValueItem is string complexStr)
1758 {
1759 element = ParseComplex(complexStr);
1760 AddToList(innerList, element, count++);
1761 }
1762 else if (kvpValueItem is Complex complexValue)
1763 {
1764 AddToList(innerList, complexValue, count++);
1765 }
1766 else if (true == IsDictionaryType(kvpValueItemType) || true == IsEnumerableType(kvpValueItemType)) // json and yaml will collapse arrays of 1 into an object
1767 {
1768 // yaml and json have a nasty habit of collapsing arrays with one elemnt to a singular type, messes us up when we expect arrays not a singular value
1769 element = CreateObjectFromDictionary(kvpValueItem, elementType, kvpValue, columnNames);
1770 AddToList(innerList, element, count++);
1771
1772 }
1773 else
1774 {
1775 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Complex format [{kvpValueItem}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1776 }
1777 }
1778 else if (((IsDictionaryType(kvpValueItemType) || IsEnumerableType(elementType) || elementType.IsArray)) && typeof(string) != kvpValueItemType)
1779 {
1780 if (IsEnumerableType(elementType) && !(kvpValueItem is IEnumerable))
1781 {
1782 // JSON collapses a single-item List<T> to a bare scalar; wrap it
1783 Type innerScalarType = elementType.IsGenericType ? elementType.GetGenericArguments()[0] : typeof(object);
1784 dynamic singleItemList = InstantiateOne(elementType);
1785 AddToList(singleItemList, ConvertValue(kvpValueItem, innerScalarType, null, kvpName), 0);
1786 element = singleItemList;
1787 }
1788 else
1789 {
1790 element = CreateObjectFromDictionary(kvpValueItem, elementType, parentDictionary, columnNames);
1791 }
1792 AddToList(innerList, element, count++);
1793 }
1794 else if (IsBigIntegerType(elementType) || IsBigIntegerType(innerElementType))
1795 {
1796 Type biNullableCheck2 = IsBigIntegerType(innerElementType) ? innerElementType : elementType;
1797 if (kvpValueItem is string biNullStr3 && IsNullableType(biNullableCheck2) && biNullStr3.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1798 AddToList(innerList, null, count++);
1799 else
1800 {
1801 try { AddToList(innerList, AsBigInteger(kvpValueItem), count++); }
1802 catch { GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid BigInteger format [{kvpValueItem}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other); }
1803 }
1804 }
1805 else if (IsVersionType(elementType) || IsVersionType(innerElementType))
1806 {
1807 if (kvpValueItem is string versionStr && System.Version.TryParse(versionStr, out System.Version version))
1808 {
1809 AddToList(innerList, version, count++);
1810 }
1811 else
1812 {
1813 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Version format [{kvpValueItem}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1814 }
1815 }
1816 else if (true == IsIpAddressType(elementType) || true == IsIpAddressType(innerElementType))
1817 {
1818 if (kvpValueItem is string ipStr && IPAddress.TryParse(ipStr, out IPAddress ipAddress))
1819 {
1820 AddToList(innerList, ipAddress, count++);
1821 }
1822 else
1823 {
1824 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid IPAddress format [{kvpValueItem}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1825 }
1826 }
1827 else if (true == IsUriType(elementType) || true == IsUriType(innerElementType) )
1828 {
1829 if (kvpValueItem is string uriStr && Uri.TryCreate(uriStr, UriKind.Absolute, out Uri uri))
1830 {
1831 AddToList(innerList, uri, count++);
1832 }
1833 else
1834 {
1835 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Uri format [{kvpValueItem}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1836 }
1837 }
1838 else if (true == IsGuidType(elementType) || true == IsGuidType(innerElementType))
1839 {
1840 Type gNullableCheck = IsGuidType(innerElementType) ? innerElementType : elementType;
1841 if (kvpValueItem is string guidNullStr && IsNullableType(gNullableCheck) && guidNullStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1842 AddToList(innerList, null, count++);
1843 else if (kvpValueItem is string guidStr && Guid.TryParse(guidStr, out Guid guid))
1844 AddToList(innerList, guid, count++);
1845 else
1846 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Guid format [{kvpValueItem}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1847 }
1848 else if (true == IsTimeSpanType(elementType) || true == IsTimeSpanType(innerElementType))
1849 {
1850 Type tsNullableCheck = IsTimeSpanType(innerElementType) ? innerElementType : elementType;
1851 if (kvpValueItem is string tsNullStr && IsNullableType(tsNullableCheck) && tsNullStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
1852 AddToList(innerList, null, count++);
1853 else if (kvpValueItem is string timeSpanStr && TimeSpan.TryParse(timeSpanStr, out TimeSpan timeSpan))
1854 AddToList(innerList, timeSpan, count++);
1855 else
1856 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid TimeSpan format [{kvpValueItem}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1857 }
1858
1859 else if (IsCustomStructType(elementType) || IsCustomStructType(innerElementType))
1860 {
1861 Type csElemType = IsCustomStructType(innerElementType) ? innerElementType : elementType;
1862 // Unwrap single-item XML list wrapper if needed
1863 dynamic csItem = kvpValueItem;
1864 if (csItem is IList csl3 && csl3.Count == 1 && csl3[0] is IDictionary csd3)
1865 csItem = csd3;
1866 if (csItem is IDictionary csStructDict3)
1867 {
1868 element = CreateObjectFromDictionary(csStructDict3, csElemType, parentDictionary, columnNames);
1869 AddToList(innerList, element, count++);
1870 }
1871 else
1872 {
1873 var converter = TypeDescriptor.GetConverter(csElemType);
1874 if (converter != null && converter.CanConvertFrom(csItem.GetType()))
1875 {
1876 element = converter.ConvertFrom(csItem);
1877 AddToList(innerList, element, count++);
1878 }
1879 else if (false == IsSimpleType(csItem.GetType()))
1880 {
1881 try
1882 {
1883 AddToList(innerList, csItem, count++);
1884 }
1885 catch
1886 {
1887 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported struct type [{csItem.GetType()}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1888 }
1889 }
1890 }
1891 }
1892 else if (IsSimpleType(elementType) || IsNullableType(elementType))
1893 {
1894 if (IsSimpleType(kvpValueItem.GetType()) || IsNullableType(kvpValueItem.GetType()))
1895 {
1896 element = ConvertValue(kvpValueItem, elementType, null, kvpName);
1897 AddToList(innerList, element, count++);
1898 }
1899 else
1900 {
1901 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Value type [{kvpValueItem.GetType()}] incompatible with simple/nullable element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1902 }
1903 }
1904 else if (IsClassType(elementType) && elementType != typeof(string))
1905 {
1906 var converter = TypeDescriptor.GetConverter(elementType);
1907 if (converter != null && converter.CanConvertFrom(kvpValueItem.GetType()))
1908 {
1909 element = converter.ConvertFrom(kvpValueItem);
1910 AddToList(innerList, element, count++);
1911 }
1912 else
1913 {
1914 try
1915 {
1916 AddToList(innerList, kvpValueItem, count++);
1917 }
1918 catch
1919 {
1920 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported class type [{kvpValueItem.GetType()}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1921 }
1922 }
1923 }
1924 else
1925 {
1926 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unsupported item type [{kvpValueItem?.GetType()}] for element type [{elementType}]", kvpValueItem, GPALObjectType.Other);
1927 }
1928 }
1929
1930 if (ObjectCopier.GetItemCount(kvpProperty, innerList) > 0)
1931 {
1932 if (IsEnumerableObject(objOrList) || objOrList.GetType().IsArray)
1933 AddToList(objOrList, innerList, count++);
1934 else if (objOrList.GetType().IsValueType)
1935 {
1936 string bfName = $"<{kvpProperty.Name}>k__BackingField";
1937 objOrList = GetSetStructMethod(objOrList.GetType()).Invoke(null, new object[] { (object)objOrList, bfName, innerList });
1938 }
1939 else
1940 kvpProperty.SetValue(objOrList, innerList);
1941 }
1942 else
1943 {
1944 string currentErrorMessage = $"No supplied values for [{kvpProperty?.Name}]";
1945 if (false == lastErrorMessage.Contains(currentErrorMessage))
1946 {
1947 lastErrorMessage.Add(currentErrorMessage);
1948 GPAL.PublishSimpleEvent(GPALEventType.ERROR, currentErrorMessage, objOrList, GPALObjectType.Other);
1949 supressedMessage = false;
1950 }
1951 else if (false == supressedMessage)
1952 {
1953 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Supressing repeat messages", objOrList, GPALObjectType.Other);
1954 supressedMessage = true;
1955 }
1956 }
1957 }
1958 else if (IsComplexType(elementType))
1959 {
1960 if (kvpValue != null)
1961 {
1962 itemobj = ParseComplex(kvpValue.ToString());
1963 AddToList(objOrList, itemobj, count++);
1964 }
1965 }
1966 else
1967 {
1968 // Scalar kvpValue for a collection property — SetValue handles wrapping it in a
1969 // single-element list, including collapsed JSON/YAML (e.g. "gamma" -> List<string>{"gamma"}).
1970 // Do NOT call AddToList(objOrList, ...) here: objOrList is the containing object,
1971 // not the collection — EnsureCollectionType would mis-read its generic args.
1972 SetValue(ref objOrList, kvpProperty, kvpValue, kvpPropertyType, parentDictionary, columnNames);
1973 }
1974 }
1975 else if (IsCustomStructType(kvpPropertyType))
1976 {
1977 // Unwrap single-element XML list wrapper if needed
1978 IDictionary structKvpDict = kvpValue as IDictionary;
1979 if (structKvpDict == null && kvpValue is IList svl && svl.Count == 1 && svl[0] is IDictionary svd)
1980 structKvpDict = svd;
1981 if (structKvpDict != null)
1982 {
1983 object structObj = CreateObjectFromDictionary(structKvpDict, kvpPropertyType, parentDictionary, columnNames);
1984 if (objOrList.GetType().IsValueType)
1985 {
1986 string bfName = $"<{kvpProperty.Name}>k__BackingField";
1987 objOrList = GetSetStructMethod(objOrList.GetType()).Invoke(null, new object[] { (object)objOrList, bfName, structObj });
1988 }
1989 else
1990 kvpProperty.SetValue(objOrList, structObj);
1991 }
1992 }
1993 else if (IsComplexType(kvpPropertyType) || IsNullableType(kvpPropertyType) || IsSimpleType(kvpPropertyType))
1994 {
1995 SetValue(ref objOrList, kvpProperty, kvpValue, kvpPropertyType, parentDictionary, columnNames);
1996 }
1997 else if (kvpProperty.Name.Equals(kvpName, StringComparison.OrdinalIgnoreCase))
1998 {
1999 SetValue(ref objOrList, kvpProperty, kvpValue, kvpPropertyType, parentDictionary, columnNames);
2000 }
2001 else if (kvpValue != null && (IsDictionaryType(kvpValue.GetType()) || IsEnumerableType(kvpValue.GetType())))
2002 {
2003 objOrList = CreateObjectFromDictionary(kvpValue, targetType, kvpValue, columnNames);
2004 }
2005 else
2006 {
2007 noPropertyOrField++;
2008 }
2009 }
2010 else if (kvpProperty != null && !kvpProperty.CanWrite && kvpProperty.Name.Equals(kvpName, StringComparison.OrdinalIgnoreCase))
2011 {
2012 if (false == targetType.Name.StartsWith("GPAL"))
2013 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{targetType.Name}][{kvpName}] 'CanWrite' [false]", objOrList, GPALObjectType.Other);
2014 }
2015 else if (kvpField != null && kvpField.Name.Equals(kvpName, StringComparison.OrdinalIgnoreCase))
2016 {
2017 if (IsEnumerableType(kvpFieldType) || kvpFieldType.IsArray)
2018 {
2019 Type elementType = kvpFieldType.IsArray ? kvpFieldType.GetElementType() : kvpFieldType.GetGenericArguments()[0];
2020 itemobj = ConverterHelper.InstantiateOne(elementType);
2021
2022 count = 0;
2023 if (IsEnumerableObject(kvpValue) || IsArray(kvpValue))
2024 {
2025 object innerList = ConverterHelper.InstantiateOne(kvpFieldType, ObjectCopier.GetItemCount(kvpProperty, kvpValue));
2026 foreach (dynamic kvpValueItem in (IEnumerable)kvpValue)
2027 {
2028 Type kvpValueItemType = kvpValueItem.GetType();
2029 object element = null;
2030
2031 if (IsKeyValuePairType(kvpValueItemType))
2032 {
2033 var nestedKvp = kvpValueItem;
2034 Type valueType = elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(Dictionary<,>)
2035 ? elementType.GetGenericArguments()[1]
2036 : elementType;
2037
2038 if (IsDictionaryType(elementType))
2039 {
2040 element = CreateObjectFromDictionary(new Dictionary<object, object> { { nestedKvp.Key, nestedKvp.Value } }, elementType, kvpValue, columnNames);
2041 }
2042 else if (IsComplexType(valueType))
2043 {
2044 element = ParseComplex(nestedKvp.Value?.ToString());
2045 }
2046 else if (IsSimpleType(valueType))
2047 {
2048 element = ConvertValue(kvp.Value?.ToString().Trim('"'), valueType);
2049 }
2050 else
2051 {
2052 element = nestedKvp.Value; // Use Value directly
2053 }
2054 AddToList(innerList, element, count++);
2055 }
2056 else if (IsDictionaryType(kvpValueItemType) || IsEnumerableType(elementType) || elementType.IsArray)
2057 {
2058 element = CreateObjectFromDictionary(kvpValueItem, elementType, kvpValue, columnNames);
2059 AddToList(innerList, element, count++);
2060 }
2061 else if (IsComplexType(elementType))
2062 {
2063 element = ParseComplex(kvpValueItem.ToString());
2064 AddToList(innerList, element, count++);
2065 }
2066 else if (IsSimpleType(elementType))
2067 {
2068 element = ConvertValue(kvpValueItem.ToString(), elementType);
2069 AddToList(innerList, Convert.ChangeType(element, elementType), count++);
2070 }
2071 else
2072 {
2073 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Skipping unexpected item type [{kvpValueItemType.Name}] for [{elementType.Name}]", kvpValueItem, GPALObjectType.None);
2074 }
2075 }
2076
2077 if (ObjectCopier.GetItemCount(kvpProperty, innerList) > 0)
2078 {
2079 if (IsEnumerableObject(objOrList) || objOrList.GetType().IsArray)
2080 AddToList(objOrList, innerList, count++);
2081 else if (objOrList.GetType().IsValueType)
2082 objOrList = GetSetStructMethod(objOrList.GetType()).Invoke(null, new object[] { (object)objOrList, kvpField.Name, (object)innerList });
2083 else
2084 kvpField.SetValue(objOrList, innerList);
2085 }
2086 else
2087 {
2088 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No values for [{kvpField.Name}]", objOrList, GPALObjectType.Other);
2089 }
2090 }
2091 else if (IsComplexType(kvpFieldType))
2092 {
2093 itemobj = ParseComplex(kvpValue.ToString());
2094 AddToList(objOrList, itemobj, count++);
2095 }
2096 else if (IsSimpleType(kvpFieldType))
2097 {
2098 itemobj = ConvertValue(kvpValue.ToString(), kvpFieldType);
2099 SetValue(ref objOrList, kvpField, itemobj, kvpFieldType, parentDictionary, columnNames);
2100 }
2101 else
2102 {
2103 SetValue(ref objOrList, kvpField, kvpValue, kvpFieldType, parentDictionary, columnNames);
2104 }
2105 }
2106 else if (IsCustomStructType(kvpFieldType) && kvpValue is IDictionary)
2107 {
2108 object structObj = CreateObjectFromDictionary(kvpValue, kvpFieldType, parentDictionary, columnNames);
2109 kvpField.SetValue(objOrList, structObj);
2110 }
2111 else if (IsComplexType(kvpFieldType) || IsNullableType(kvpFieldType) || IsSimpleType(kvpFieldType))
2112 {
2113 SetValue(ref objOrList, kvpField, kvpValue, kvpFieldType, parentDictionary, columnNames);
2114 }
2115 else if (kvpField.Name.Equals(kvpName, StringComparison.OrdinalIgnoreCase))
2116 {
2117 SetValue(ref objOrList, kvpField, kvpValue, kvpFieldType, parentDictionary, columnNames);
2118 }
2119 else
2120 {
2121 noPropertyOrField++;
2122 }
2123 }
2124
2125 // NOTE: dunno why i coded this, maybe an earlier (resolved bug) - instantiateone will do it's best, but maybe won't find the properties? iunno...
2126 //if (objOrList.GetType() != targetType)
2127 //{
2128 // if (kvpProperty != null && kvpProperty.CanWrite)
2129 // {
2130 // if (objOrList != null && (IsEnumerableObject(kvpProperty) || kvpPropertyType.IsArray) && ObjectCopier.GetItemCount(kvpProperty, objOrList) > 0)
2131 // kvpProperty.SetValue(objOrList, objOrList);
2132 // else if (itemobj != null)
2133 // kvpProperty.SetValue(objOrList, itemobj);
2134 // }
2135 // else if (kvpField != null)
2136 // {
2137 // if (objOrList != null && (IsEnumerableObject(kvpField) || kvpFieldType.IsArray) && ObjectCopier.GetItemCount(kvpField, objOrList) > 0)
2138 // kvpField.SetValue(objOrList, objOrList);
2139 // else if (itemobj != null)
2140 // kvpField.SetValue(objOrList, itemobj);
2141 // }
2142 //}
2143 //else
2144 if (noPropertyOrField == 2)
2145 {
2146 // can be excessive as ERROR for iterating over large collections that don't map
2147 // we will assume the user knows these won't be there but only need to emit that once
2148 string currentErrorMessage = $"No property/field [{kvpName}] in [{targetType.Name}]";
2149 if (false == lastErrorMessage.Contains(currentErrorMessage))
2150 {
2151 lastErrorMessage.Add(currentErrorMessage);
2152 GPAL.PublishSimpleEvent(GPALEventType.WARNING, currentErrorMessage, objOrList, GPALObjectType.Other);
2153 supressedMessage = false;
2154 }
2155 else if (false == supressedMessage)
2156 {
2157 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Supressing repeat messages", objOrList, GPALObjectType.Other);
2158 supressedMessage = true;
2159 }
2160 }
2161 }
2162 }
2163
2164 if (addedToVisited) _dictVisited.Remove((object)dictionary);
2165 if (isRoot) _dictVisited = null;
2166 return objOrList;
2167 }
2168 private static readonly Dictionary<Type, MethodInfo> _setStructMethodCache = new Dictionary<Type, MethodInfo>();
2169 private static MethodInfo GetSetStructMethod(Type structType)
2170 {
2171 if (!_setStructMethodCache.TryGetValue(structType, out var m))
2172 {
2173 m = typeof(ConverterHelper)
2174 .GetMethod("SetStructBackingField", BindingFlags.NonPublic | BindingFlags.Static)
2175 .MakeGenericMethod(structType);
2176 _setStructMethodCache[structType] = m;
2177 }
2178 return m;
2179 }
2180 // Takes struct BY VALUE so __makeref points to the actual unboxed local — the only reliable
2181 // way to mutate a struct field via reflection in .NET 4.8 without boxing loss.
2182 // Returns the modified struct as object; caller assigns it back to the ref dynamic.
2183 // Works for both auto-property backing fields (<Name>k__BackingField, NonPublic)
2184 // and plain public fields (ValueTuple.Item1/Item2/..., Public).
2185 private static object SetStructBackingField<T>(T instance, string fieldName, object value) where T : struct
2186 {
2187 FieldInfo field = typeof(T).GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance)
2188 ?? typeof(T).GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);
2189 if (field != null)
2190 field.SetValueDirect(__makeref(instance), value);
2191 return instance;
2192 }
2193 private static bool IsList(object obj)
2194 {
2195 return obj is IList && obj.GetType().IsGenericType;
2196 }
2197 private static bool IsArray(object obj)
2198 {
2199 return obj != null && obj.GetType().IsArray;
2200 }
2201 // for structs - value types
2202 static object SetValueDirect(FieldInfo field, object instance, object value)
2203 {
2204 field.SetValueDirect(__makeref(instance), value);
2205
2206 return instance;
2207 }
2208
2218 // could be property or field
2219 internal static void SetValue(ref dynamic instance, dynamic propertyOrField, dynamic propertyValue, Type propertyType, dynamic parentDictionary, List<string> columnNames)
2220 {
2221 // "Recursion to [...]" is a placeholder written for shared/cyclic object references
2222 // (see ConvertClassToDictionary). It cannot be converted back into the original
2223 // (non-string) type, so leave the property/field at its default value.
2224 if (propertyValue is string recursionMarker && propertyType != typeof(string)
2225 && recursionMarker.StartsWith("Recursion to [") && recursionMarker.EndsWith("]"))
2226 return;
2227
2228 // XML parsers wrap single child elements in a List<object>. When the target property
2229 // is a non-enumerable class/struct, unwrap the single-element list to the inner dict
2230 // so the IDictionary path below can reconstruct it properly.
2231 if (propertyValue is IList svSingleList && svSingleList.Count == 1
2232 && svSingleList[0] is IDictionary svInnerDict
2233 && !IsEnumerableType(propertyType) && !propertyType.IsArray && !IsDictionaryType(propertyType))
2234 {
2235 propertyValue = svInnerDict;
2236 }
2237
2238 dynamic entryValue = null;
2239 string propertyTypeName = propertyType.Name;
2240
2241 // XML serializes arrays/lists as <item>...</item> children. StripGpalKeys may leave
2242 // the value as a single-item list wrapping a single-key dict, or directly as a
2243 // single-key dict. Unwrap both patterns so the enumerable-handling code below sees
2244 // the actual sequence.
2245 if ((IsEnumerableType(propertyType) || propertyType.IsArray) && !IsDictionaryType(propertyType))
2246 {
2247 // Pattern: List<object>{ {item: [v1, v2, ...]} }
2248 if (propertyValue is IList pvList && pvList.Count == 1 && pvList[0] is IDictionary pvInner && pvInner.Count == 1)
2249 {
2250 foreach (DictionaryEntry xw in pvInner)
2251 {
2252 propertyValue = xw.Value is IList ? xw.Value : new List<object> { xw.Value };
2253 break;
2254 }
2255 }
2256 // Pattern: {item: [v1, v2, ...]}
2257 else if (propertyValue is IDictionary xmlWrapperDict && xmlWrapperDict.Count == 1)
2258 {
2259 foreach (DictionaryEntry xw in xmlWrapperDict)
2260 {
2261 propertyValue = xw.Value is IList ? xw.Value : new List<object> { xw.Value };
2262 break;
2263 }
2264 }
2265 }
2266
2267 if (typeof(IPAddress) == propertyType)
2268 {
2269 if (propertyValue is IDictionary ipDict)
2270 {
2271 foreach (var v in ipDict.Values) { entryValue = v; break; }
2272 }
2273 else
2274 entryValue = propertyValue?.ToString();
2275 }
2276 else if (IsTupleType(propertyType) && propertyValue is IDictionary)
2277 {
2278 // Tuple property supplied as a nested dict — reconstruct then set directly.
2279 // Bypasses ValueToString, which expects a real tuple value, not a dict.
2280 object tupleValue = CreateObjectFromDictionary(propertyValue, propertyType, parentDictionary, columnNames);
2281 if (IsValueType(instance.GetType()))
2282 {
2283 if (propertyOrField is PropertyInfo tupleProp)
2284 {
2285 string bfName = $"<{tupleProp.Name}>k__BackingField";
2286 instance = GetSetStructMethod(instance.GetType()).Invoke(null, new[] { instance, bfName, tupleValue });
2287 }
2288 else if (propertyOrField is FieldInfo tupleField)
2289 instance = GetSetStructMethod(instance.GetType()).Invoke(null, new[] { instance, tupleField.Name, tupleValue });
2290 }
2291 else
2292 propertyOrField.SetValue(instance, tupleValue);
2293 return;
2294 }
2295 else if (IsComplexType(propertyType) && propertyValue is IEnumerable && !(propertyValue is string))
2296 {
2297 // Complex supplied as a dict/list from XML — extract Real/Imaginary directly.
2298 // Bypasses ValueToString (expects a real Complex) and CreateObjectFromDictionary
2299 // (fails on XML list wrappers and string-typed values from XML text nodes).
2300 propertyOrField.SetValue(instance, ComplexFromDict(propertyValue));
2301 return;
2302 }
2303 else
2304 entryValue = ValueToString(propertyValue, propertyType);
2305
2306 if (true == IsValueType(instance.GetType()))
2307 {
2308 // Cycle sentinel: {"AnyKey": "Recursion to [...]"} — produced by ConvertClassToDictionary when a
2309 // cycle is detected. Converting the whole dict to the target type would cause COFD to report
2310 // "No property/field [AnyKey]" on the target. Collapse it to null here.
2311 if (propertyValue is IDictionary svCycleD && svCycleD.Count == 1 && !IsDictionaryType(propertyType))
2312 {
2313 foreach (DictionaryEntry svce in svCycleD)
2314 {
2315 if (svce.Value is string svceStr && svceStr.StartsWith("Recursion to ["))
2316 {
2317 propertyValue = null;
2318 entryValue = null;
2319 }
2320 break;
2321 }
2322 }
2323
2324 if (propertyOrField is FieldInfo)
2325 {
2326 // ValueTuple uses public fields (Item1/Item2/...); auto-properties use NonPublic backing fields.
2327 // SetStructBackingField<T> searches both, so route all struct field sets through it.
2328 object converted;
2329 if (propertyValue is IDictionary && !IsSimpleType(propertyType))
2330 converted = CreateObjectFromDictionary(propertyValue, propertyType, null, null);
2331 else
2332 converted = ConvertValue(entryValue, propertyType);
2333 instance = GetSetStructMethod(instance.GetType()).Invoke(null, new[] { instance, ((FieldInfo)propertyOrField).Name, converted });
2334 }
2335 else
2336 {
2337 // PropertyInfo.SetValue on a boxed struct (NET 4.8) silently discards the result —
2338 // it sets on a copy, not the box. Go through the compiler-generated backing field,
2339 // which FieldInfo.SetValue correctly writes in-place on the box.
2340 object converted;
2341 if (propertyValue is IDictionary && !IsSimpleType(propertyType))
2342 converted = CreateObjectFromDictionary(propertyValue, propertyType, null, null);
2343 else
2344 converted = ConvertValue(entryValue, propertyType);
2345 string backingName = $"<{propertyOrField.Name}>k__BackingField";
2346 FieldInfo bf = instance.GetType().GetField(backingName, BindingFlags.NonPublic | BindingFlags.Instance);
2347 if (bf != null)
2348 // Generic helper unboxes to a real local so __makeref is valid; returns updated struct.
2349 // Assigning back to ref dynamic propagates to objOrList in CreateObjectFromDictionary.
2350 instance = GetSetStructMethod(instance.GetType()).Invoke(null, new[] { instance, backingName, converted });
2351 else
2352 propertyOrField.SetValue(instance, converted);
2353 }
2354 }
2355 else if (true == IsVersionType(propertyType)) // must be before simple check
2356 {
2357 propertyOrField.SetValue(instance, ConvertValue(propertyValue, propertyType));
2358 }
2359 else if (IsSimpleType(propertyType) || IsEnumType(propertyType)) // <-- added enum support for scalar properties
2360 {
2361 if (null != propertyOrField)
2362 {
2363 if (typeof(object) == instance.GetType())
2364 instance = $"{propertyOrField}{ConverterSettings.OutDelimiter}{propertyValue}";
2365 else
2366 {
2367 try
2368 {
2369 object convertedValue = ConvertValue(entryValue, propertyType);
2370 propertyOrField.SetValue(instance, convertedValue);
2371 }
2372 catch
2373 {
2374 // Fallback: just set raw value (useful for dynamic/expando objects)
2375 propertyOrField.SetValue(instance, propertyValue);
2376 }
2377 }
2378 }
2379 else if (typeof(string) == instance?.GetType())
2380 instance = propertyValue;
2381 else
2382 instance = propertyValue; // instance is prolly dynamic
2383 }
2384 else if (true == IsBigIntegerType(propertyType))
2385 {
2386 BigInteger bigInteger;
2387 if (true == BigInteger.TryParse(entryValue, out bigInteger))
2388 propertyOrField.SetValue(instance, bigInteger);
2389 }
2390 else if (true == IsComplexType(propertyType))
2391 {
2392 dynamic complex = ParseComplex(entryValue);
2393 if (null != complex)
2394 propertyOrField.SetValue(instance, complex);
2395 }
2396 else if (true == IsNullableType(propertyType))
2397 {
2398 Type underlyingType = Nullable.GetUnderlyingType(propertyType);
2399 dynamic underlyingValueObject = InstantiateOne(underlyingType);
2400 SetValue(ref instance, propertyOrField, entryValue, underlyingType, parentDictionary, columnNames);
2401 }
2402 else if (IsDictionaryType(propertyType))
2403 {
2404 object value = CreateObjectFromDictionary(propertyValue, propertyType, parentDictionary, columnNames);
2405 propertyOrField.SetValue(instance, value);
2406 }
2407 else if (true == IsEnumerableType(propertyType) || true == propertyType.IsArray)
2408 {
2409 dynamic enumeration = null;
2410
2411 // can't add null to a list
2412 if (null != propertyValue)
2413 {
2414 // "null" string from XML means the collection is null — leave at default.
2415 if (propertyValue is string svNullEnumStr && svNullEnumStr.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
2416 enumeration = null;
2417 // strings implement IEnumerable<char>; dicts implement IEnumerable<KVP> —
2418 // neither should be iterated as a sequence of collection elements.
2419 else if ((true == IsEnumerableObject(propertyValue) && !(propertyValue is string) && !IsDictionaryType(propertyValue.GetType())) || true == IsArray(propertyValue))
2420 enumeration = (IEnumerable)propertyValue;
2421 else
2422 {
2423 Type _elemType = propertyType.GetElementType()
2424 ?? (propertyType.IsGenericType ? propertyType.GetGenericArguments()[0] : null);
2425 bool _compatible = _elemType == null
2426 || _elemType.IsAssignableFrom(propertyValue.GetType())
2427 || IsSimpleType(_elemType)
2428 || IsDictionaryType(propertyValue.GetType());
2429
2430 if (_compatible)
2431 {
2432 // If the scalar is a dict and the element type is a class/struct, convert it
2433 // before adding — don't add the raw dictionary to List<SomeType>.
2434 object _singleItem = (_elemType != null && propertyValue is IDictionary && !IsDictionaryType(_elemType))
2435 ? CreateObjectFromDictionary(propertyValue, _elemType, parentDictionary, columnNames)
2436 : (object)propertyValue;
2437 enumeration = InstantiateOne(propertyType, 1);
2438 // Use AddToList (reflection-based) rather than dynamic .Add() — DLR fails to resolve
2439 // List<StructType>.Add(StructType) when _singleItem is typed as object (boxed struct).
2440 if (true == IsEnumerableObject(enumeration) && false == IsArray(enumeration))
2441 AddToList(enumeration, _singleItem, 0);
2442 else if (true == IsArray(enumeration))
2443 enumeration[0] = _singleItem;
2444 }
2445 else
2446 {
2447 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Scalar [{propertyValue?.GetType()}] is not compatible with collection element type [{_elemType}]. Leaving [{propertyType}] at default.", propertyValue, GPALObjectType.Other);
2448 enumeration = InstantiateOne(propertyType, 0);
2449 }
2450 }
2451
2452 if (enumeration == null)
2453 enumeration = InstantiateOne(propertyType, 0);
2454
2455 int count = 0;
2456 foreach (dynamic item in enumeration)
2457 count++;
2458
2459 dynamic list = InstantiateOne(propertyType, count);
2460
2461 count = 0;
2462 foreach (dynamic item in enumeration)
2463 {
2464 if (false == IsCustomStructType(item?.GetType()) && null == item)
2465 {
2466 Type _elemType = propertyType.GetElementType() ?? (propertyType.IsGenericType ? propertyType.GetGenericArguments()[0] : null);
2467 bool _canBeNull = _elemType == null || !_elemType.IsValueType || IsNullableType(_elemType);
2468 if (_canBeNull)
2469 AddToList(list, null, count++);
2470 continue;
2471 }
2472
2473 dynamic itemValue = item;
2474 dynamic itemKey = null;
2475 Type itemType = item.GetType();
2476 Type objectElementType = propertyType.GetElementType();
2477 if (null == objectElementType)
2478 objectElementType = propertyType.GetGenericArguments()[0];
2479
2480 if (true == IsKeyValuePairType(itemType))
2481 {
2482 itemKey = item.Key;
2483 itemValue = item.Value;
2484 itemType = item.Value.GetType();
2485 }
2486
2487 if (true == IsComplexType(objectElementType))
2488 {
2489 dynamic complex = ParseComplex(itemValue.ToString());
2490 if (null != complex)
2491 AddToList(list, complex, count++);
2492 }
2493 else if (true == IsSimpleType(objectElementType) || IsEnumType(objectElementType)) // <-- enum support for collection elements
2494 {
2495 dynamic tempVal = ConvertValue(itemValue.ToString(), objectElementType, null, propertyOrField?.Name);
2496 AddToList(list, tempVal, count++);
2497 }
2498 else if (IsEnumerableType(objectElementType))
2499 {
2500 dynamic myList = InstantiateOne(objectElementType);
2501 Type elementType = objectElementType.GenericTypeArguments[0];
2502 int innerCount = 0;
2503
2504 // Unwrap XML item-wrapper: single-item-list wrapping a single-key dict,
2505 // or a bare single-key dict — both are {item:[...]} artifacts from XML.
2506 dynamic resolvedItemValue = itemValue;
2507 if (resolvedItemValue is IList rilvSingle && rilvSingle.Count == 1 && rilvSingle[0] is IDictionary rilvInner && rilvInner.Count == 1)
2508 {
2509 foreach (DictionaryEntry rkv in rilvInner) { resolvedItemValue = rkv.Value is IList ? rkv.Value : new List<object> { rkv.Value }; break; }
2510 }
2511 else if (resolvedItemValue is IDictionary rilvDict && rilvDict.Count == 1)
2512 {
2513 foreach (DictionaryEntry rkv in rilvDict) { resolvedItemValue = rkv.Value is IList ? rkv.Value : new List<object> { rkv.Value }; break; }
2514 }
2515
2516 // A scalar (e.g. JSON long) that is not IEnumerable must be wrapped before the
2517 // inner iteration; casting a non-IEnumerable to IEnumerable<dynamic> throws.
2518 if (resolvedItemValue != null && !(resolvedItemValue is IEnumerable))
2519 {
2520 resolvedItemValue = new List<object> { resolvedItemValue };
2521 }
2522
2523 if (true == IsComplexType(elementType))
2524 {
2525 Complex complex;
2526
2527 foreach (object item2 in (IEnumerable<dynamic>)resolvedItemValue)
2528 {
2529 dynamic iv2 = item2;
2530 if (iv2 is IList il2 && ((IList)il2).Count == 1) iv2 = ((IList)il2)[0];
2531 complex = ParseComplex(iv2?.ToString());
2532 if (null != complex)
2533 AddToList(myList, complex, innerCount++);
2534 }
2535 }
2536 else if (IsSimpleType(elementType) || IsEnumType(elementType) || IsBigIntegerType(elementType))
2537 {
2538 foreach (object item2 in (IEnumerable<dynamic>)resolvedItemValue)
2539 {
2540 dynamic iv2 = item2;
2541 if (iv2 is IList il2 && ((IList)il2).Count == 1) iv2 = ((IList)il2)[0];
2542 dynamic convItem = IsBigIntegerType(elementType)
2543 ? System.Numerics.BigInteger.Parse(iv2?.ToString())
2544 : iv2;
2545 AddToList(myList, convItem, innerCount++);
2546 }
2547 }
2548 list[count++] = myList;
2549 }
2550 else if (itemValue is IDictionary itemDictValue && !IsDictionaryType(objectElementType))
2551 {
2552 // class/struct/tuple element — deserialize the dict into the target type
2553 dynamic converted = CreateObjectFromDictionary(itemDictValue, objectElementType, parentDictionary, columnNames);
2554 AddToList(list, converted, count++);
2555 }
2556 else
2557 {
2558 list[count++] = itemValue; // fallback – use raw value
2559 }
2560 }
2561 if (0 < count)
2562 propertyOrField.SetValue(instance, list);
2563 }
2564 }
2565 else if (true == IsClassType(propertyType))
2566 {
2567 dynamic value = null;
2568 Dictionary<dynamic, dynamic> myDict = new Dictionary<dynamic, dynamic>();
2569 try
2570 {
2571 if (null != propertyValue)
2572 foreach (dynamic item in propertyValue)
2573 if (true == IsDictionaryType(item.GetType()))
2574 {
2575 myDict = item;
2576 value = ConvertToClass(new List<Dictionary<dynamic, dynamic>>() { myDict }, propertyType, parentDictionary, columnNames);
2577 }
2578 else
2579 myDict[item.Key] = item.Value;
2580
2581 // no input for this member means the member is null, not an empty instance.
2582 // converting the empty myDict here handed back a default-constructed object,
2583 // so a written "parent: null" read back as an object with every field at default
2584 if (null == value && null != propertyValue)
2585 value = ConvertToClass(new List<Dictionary<dynamic, dynamic>>() { myDict }, propertyType, parentDictionary, columnNames);
2586 }
2587 catch (Exception)
2588 {
2589 try
2590 {
2591 IDictionary catchDict = propertyValue as IDictionary;
2592 if (catchDict == null && propertyValue is IList pvCatchList)
2593 {
2594 foreach (var pvItem in pvCatchList)
2595 if (pvItem is IDictionary d) { catchDict = d; break; }
2596 }
2597 if (catchDict != null)
2598 value = ConvertToClass(new List<Dictionary<dynamic, dynamic>>() { (Dictionary<dynamic, dynamic>)(object)catchDict }, propertyType, parentDictionary, columnNames);
2599 }
2600 catch { }
2601 }
2602 propertyOrField.SetValue(instance, value);
2603 }
2604 else
2605 {
2606 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unknown object type [{propertyType}]", propertyValue, GPALObjectType.Other);
2607 }
2608 }
2609 private static bool IsSupportedCollectionType(Type type)
2610 {
2611 return type.IsArray ||
2612 (type.IsGenericType && (
2613 type.GetGenericTypeDefinition() == typeof(List<>) ||
2614 type.GetGenericTypeDefinition() == typeof(IList<>) ||
2615 type.GetGenericTypeDefinition() == typeof(ICollection<>)
2616 ));
2617 }
2618
2627 internal static bool IsCustomStructType(Type type)
2628 {
2629 try
2630 {
2631 if (type == null ||
2632 !type.IsValueType ||
2633 type.IsPrimitive ||
2634 type.IsEnum)
2635 {
2636 return false;
2637 }
2638
2639 // Explicitly exclude known system structs (safety net)
2640 if (IsKnownSystemStruct(type))
2641 {
2642 return false;
2643 }
2644
2645 // Optional: also reject types from core runtime assemblies if namespace is empty or unexpected
2646 // (rare, but covers some internal structs)
2647 if (type.Assembly == typeof(object).Assembly) // System.Private.CoreLib
2648 {
2649 return false;
2650 }
2651
2652 if (true == IsRegisteredStruct(type))
2653 return true;
2654 }
2655 catch
2656 { }
2657
2658 return false; // we do not know
2659 }
2660
2661 internal static bool IsRegisteredStruct(Type type)
2662 {
2663 return type != null && ConverterSettings.RegisteredStructs.Contains(type);
2664 }
2665
2666 private static bool IsKnownSystemStruct(Type type)
2667 {
2668 // Widely available pre-.NET 6 system structs
2669 if (type == typeof(decimal) ||
2670 type == typeof(DateTime) ||
2671 type == typeof(DateTimeOffset) ||
2672 type == typeof(TimeSpan) ||
2673 type == typeof(TimeZoneInfo) ||
2674 type == typeof(Guid) ||
2675 type == typeof(BigInteger) ||
2676 type == typeof(Complex))
2677 {
2678 return true;
2679 }
2680
2681 // Primary filter: reject anything in System.* or Microsoft.* namespaces
2682 string ns = type?.Namespace ?? string.Empty;
2683 if (ns.StartsWith("System") || ns.StartsWith("Microsoft"))
2684 {
2685 return false;
2686 }
2687 // Tuples
2688 if (ns == "System" && true == type?.Name.StartsWith("ValueTuple"))
2689 return true;
2690
2691 // Nullable<T>
2692 if (true == type?.IsGenericType && type?.GetGenericTypeDefinition() == typeof(Nullable<>))
2693 return true;
2694
2695 // System.Numerics (Vector<T>, Matrix, etc.)
2696 if (ns == "System.Numerics")
2697 return true;
2698
2699 // Half (.NET 5+)
2700 if (type?.Name == "Half" && ns == "System")
2701 return true;
2702
2703 return false;
2704 }
2705
2706 internal static bool IsArrayType(Type type)
2707 {
2708 return (bool)type?.IsArray;
2709 }
2710
2711 internal static bool IsClassType(Type type)
2712 {
2713 if (null == type)
2714 return false;
2715
2716 return (bool)type?.IsClass && (bool)!type?.IsInterface;
2717 }
2718 internal static bool IsUriType(Type type)
2719 {
2720 return type == typeof(Uri);
2721 }
2722 internal static bool IsSimpleType(Type type)
2723 {
2724 if (null == type)
2725 return true;
2726
2727 if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
2728 return false; // KeyValuePair is NOT simple — we need to recurse into it
2729 else
2730 return type.IsPrimitive ||
2731 type.IsValueType || // CAVEAT: complex is a valuetype and should be checked
2732 type == typeof(object) || // CAVEAT, this could be trouble - we use Dictionary<dynamic, dynamic> which are mostly strings, but we may do the simple type test after all others
2733 type == typeof(string) ||
2734 type == typeof(decimal) ||
2735 type == typeof(Uri) ||
2736 type == typeof(System.Net.IPAddress) ||
2737 type == typeof(System.Version) ||
2738 type == typeof(System.Numerics.BigInteger) ||
2739 type == typeof(bool) ||
2740 type == typeof(Boolean) ||
2741 type == typeof(byte) ||
2742 type == typeof(sbyte) ||
2743 type == typeof(char) ||
2744 type == typeof(short) ||
2745 type == typeof(ushort) ||
2746 type == typeof(int) ||
2747 type == typeof(uint) ||
2748 type == typeof(long) ||
2749 type == typeof(ulong) ||
2750 type == typeof(float) ||
2751 type == typeof(double) ||
2752 type == typeof(DateTime) ||
2753 type == typeof(DateTimeOffset)
2754 ;
2755 }
2756 internal static bool IsValueType(Type type)
2757 {
2758 return type.IsValueType && !type.IsEnum && !type.IsClass && !type.IsInterface;
2759 }
2760 // Column names are positional headers for a flat/delimited row with no named keys - they don't
2761 // apply to YAML/JSON/XML, which already carry real key names.
2762 internal static bool IsDelimitedDataFormat(DataFormat format)
2763 {
2764 return format == DataFormat.CARET
2765 || format == DataFormat.COLON
2766 || format == DataFormat.CSV
2767 || format == DataFormat.CUSTOM_DELIMITER
2768 || format == DataFormat.DOT
2769 || format == DataFormat.HYPHEN
2770 || format == DataFormat.PIPE
2771 || format == DataFormat.PRN
2772 || format == DataFormat.SEMICOLON
2773 || format == DataFormat.SPACE
2774 || format == DataFormat.TAB;
2775 }
2776 // A List converted to a dictionary synthesizes GPALKEY-prefixed sequential keys internally to
2777 // stand in for list position - that's an internal sentinel, not a value meant to reach the final
2778 // key. Strip it and hand the plain index to ConvertValue so the target key type decides its own
2779 // representation (a genuine int for an int/object key, an unpadded "0"/"1"/... for a string key).
2780 internal static object ResolveSimpleDictionaryKey(dynamic rawKey, Type keyType)
2781 {
2782 string rawKeyStr = rawKey as string;
2783 if (rawKeyStr != null && rawKeyStr.StartsWith("GPALKEY", StringComparison.Ordinal) && int.TryParse(rawKeyStr.Substring(7), out int gpalKeyIdx))
2784 return ConvertValue(gpalKeyIdx, keyType);
2785
2786 if (rawKey != null && keyType.IsAssignableFrom(((object)rawKey).GetType()))
2787 return (object)rawKey;
2788
2789 return ConvertValue(rawKey?.ToString(), keyType);
2790 }
2791 internal static bool IsNullableType(Type type)
2792 {
2793 return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
2794 }
2795 private static bool IsKeyValuePairType(Type type)
2796 {
2797 bool retVal = false;
2798 try
2799 {
2800 retVal = ((bool)type?.IsGenericType && type?.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) || type == typeof(DictionaryEntry);
2801 }
2802 catch { }
2803
2804 return retVal;
2805 }
2806 internal static bool IsDictionaryType(Type type)
2807 {
2808 if (null == type)
2809 return false;
2810
2811 return typeof(IDictionary).IsAssignableFrom(type);
2812 }
2813 public static bool IsBigIntegerType(Type type)
2814 {
2815 return typeof(BigInteger) == type || typeof(BigInteger?) == type;
2816 }
2817
2818 // Converts a value to BigInteger; handles BigInteger, string, and any numeric type (int, long, etc.)
2819 // Throws if conversion is not possible.
2820 private static BigInteger AsBigInteger(object value)
2821 {
2822 if (value is BigInteger bi) return bi;
2823 if (value is string s) return BigInteger.Parse(s, NumberStyles.Integer, CultureInfo.InvariantCulture);
2824 return new BigInteger(Convert.ToInt64(value, CultureInfo.InvariantCulture));
2825 }
2826 public static bool IsByteType(Type type)
2827 {
2828 return typeof(Byte) == type || typeof(Byte?) == type;
2829 }
2830 public static bool IsBooleanType(Type type)
2831 {
2832 return typeof(Boolean) == type || typeof(Boolean?) == type;
2833 }
2834 public static bool IsDateTimeType(Type type)
2835 {
2836 return typeof(DateTime) == type || typeof(DateTime?) == type;
2837 }
2838 public static bool IsDateTimeOffsetType(Type type)
2839 {
2840 return typeof(DateTimeOffset) == type || typeof(DateTimeOffset?) == type;
2841 }
2842 public static bool IsIpAddressType(Type type)
2843 {
2844 return typeof(IPAddress) == type;
2845 }
2846 public static bool IsTimeSpanType(Type type)
2847 {
2848 return typeof(TimeSpan) == type || typeof(TimeSpan?) == type;
2849 }
2850 private static bool IsTupleType(Type type)
2851 {
2852 if (type == null || !type.IsGenericType) return false;
2853 var openType = type.GetGenericTypeDefinition();
2854
2855 // ValueTuples are structs; Tuples are classes.
2856 // Both are often unwanted when looking for "standard" structs.
2857 return openType.Namespace == "System" &&
2858 (openType.Name.StartsWith("ValueTuple`") || openType.Name.StartsWith("Tuple`"));
2859 }
2860
2861 public static bool IsGuidType(Type type)
2862 {
2863 return typeof(Guid) == type || typeof(Guid?) == type;
2864 }
2865 public static bool IsVersionType(Type type)
2866 {
2867 return typeof(System.Version) == type;
2868 }
2869 public static bool IsEnumerableType(Type type)
2870 {
2871 if (null == type)
2872 return false;
2873
2874 return (type.IsArray || typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string));
2875 }
2896 public static int CountGraphNodes(object root, int limit, out bool cyclic)
2897 {
2898 int count = 0;
2899 bool foundCycle = false;
2900
2901 CountGraphNodesRec(root, limit, ref count, ref foundCycle, new HashSet<object>(ReferenceComparer.Instance));
2902
2903 cyclic = foundCycle;
2904
2905 return count;
2906 }
2907
2914 public static int CountGraphNodes(object root, int limit)
2915 {
2916 return CountGraphNodes(root, limit, out bool _);
2917 }
2918
2919 private static void CountGraphNodesRec(object node, int limit, ref int count, ref bool cyclic, HashSet<object> onPath)
2920 {
2921 if (true == cyclic || null == node || (0 < limit && count >= limit))
2922 return;
2923
2924 Type nodeType = node.GetType();
2925
2926 // only a reference can be the same node twice. a struct is copied into the graph and a string may be
2927 // interned, so neither says anything about a cycle by turning up again
2928 bool tracked = false == nodeType.IsValueType && false == (node is string);
2929
2930 if (true == tracked && false == onPath.Add(node))
2931 {
2932 cyclic = true;
2933 return;
2934 }
2935
2936 count++;
2937
2938 if (0 >= limit || count < limit)
2939 {
2940 if (node is IDictionary dictionary)
2941 {
2942 foreach (object value in dictionary.Values)
2943 {
2944 CountGraphNodesRec(value, limit, ref count, ref cyclic, onPath);
2945
2946 if (true == cyclic || (0 < limit && count >= limit))
2947 break;
2948 }
2949 }
2950 else if (node is IEnumerable enumerable)
2951 {
2952 foreach (object value in enumerable)
2953 {
2954 CountGraphNodesRec(value, limit, ref count, ref cyclic, onPath);
2955
2956 if (true == cyclic || (0 < limit && count >= limit))
2957 break;
2958 }
2959 }
2960 else
2961 {
2962 foreach (MemberInfo member in CyclableMembers(nodeType))
2963 {
2964 CountGraphNodesRec(MemberValue(member, node), limit, ref count, ref cyclic, onPath);
2965
2966 if (true == cyclic || (0 < limit && count >= limit))
2967 break;
2968 }
2969 }
2970 }
2971
2972 // off the path on the way back up, so the same node reached twice side by side is sharing rather than
2973 // a cycle. only a node reached from inside itself is a cycle
2974 if (true == tracked)
2975 onPath.Remove(node);
2976 }
2977
2983 private static MemberInfo[] CyclableMembers(Type type)
2984 {
2985 return graphMembers.GetOrAdd(type, describing =>
2986 {
2987 List<MemberInfo> worthWalking = new List<MemberInfo>();
2988
2989 foreach (PropertyInfo property in describing.GetProperties(BindingFlags.Public | BindingFlags.Instance))
2990 if (true == property.CanRead && 0 == property.GetIndexParameters().Length && true == CanTakePartInACycle(property.PropertyType))
2991 worthWalking.Add(property);
2992
2993 foreach (FieldInfo field in describing.GetFields(BindingFlags.Public | BindingFlags.Instance))
2994 if (true == CanTakePartInACycle(field.FieldType))
2995 worthWalking.Add(field);
2996
2997 return worthWalking.ToArray();
2998 });
2999 }
3000
3007 private static bool CanTakePartInACycle(Type type)
3008 {
3009 return false == (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(DateTime)
3010 || type == typeof(DateTimeOffset) || type == typeof(decimal) || type == typeof(Guid)
3011 || type == typeof(TimeSpan) || type == typeof(Uri));
3012 }
3013
3021 private static object MemberValue(MemberInfo member, object node)
3022 {
3023 object retVal = null;
3024
3025 try
3026 {
3027 retVal = member is PropertyInfo property ? property.GetValue(node) : ((FieldInfo)member).GetValue(node);
3028 }
3029 catch (Exception)
3030 {
3031 // a getter that throws is the type's business, not the walk's
3032 }
3033
3034 return retVal;
3035 }
3036
3037 private static readonly System.Collections.Concurrent.ConcurrentDictionary<Type, MemberInfo[]> graphMembers
3038 = new System.Collections.Concurrent.ConcurrentDictionary<Type, MemberInfo[]>();
3039
3043 private sealed class ReferenceComparer : IEqualityComparer<object>
3044 {
3045 internal static readonly ReferenceComparer Instance = new ReferenceComparer();
3046
3047 public new bool Equals(object left, object right)
3048 {
3049 return ReferenceEquals(left, right);
3050 }
3051
3052 public int GetHashCode(object of)
3053 {
3054 return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(of);
3055 }
3056 }
3057
3058 public static bool IsEnumerableObject(dynamic dynamicObject)
3059 {
3060 try
3061 {
3062 if (null == dynamicObject)
3063 return false;
3064 }
3065 catch
3066 {
3067 return false;
3068 }
3069 return (typeof(IEnumerable).IsAssignableFrom(dynamicObject?.GetType()) || dynamicObject?.GetType().IsArray) && typeof(string) != dynamicObject?.GetType();
3070 }
3071 private static bool IsEnumType(Type type)
3072 {
3073 Type underlying = Nullable.GetUnderlyingType(type) ?? type;
3074 return underlying.IsEnum;
3075 }
3076 internal static bool IsComplexType(Type type)
3077 {
3078 return type == typeof(System.Numerics.Complex);
3079 }
3080 private static bool IsCompatibleType(Type targetType, Type valueType)
3081 {
3082 if (valueType == null || targetType == null) return false;
3083
3084 if (targetType.IsAssignableFrom(valueType)) return true;
3085
3086 if (IsSimpleType(targetType) && IsSimpleType(valueType)) return true;
3087
3088 if (IsNullableType(targetType))
3089 {
3090 var underlyingType = Nullable.GetUnderlyingType(targetType);
3091 return IsSimpleType(underlyingType) && IsSimpleType(valueType);
3092 }
3093
3094 if (IsComplexType(targetType) && valueType == typeof(string)) return true;
3095
3096 if (IsDictionaryType(targetType) && typeof(IDictionary).IsAssignableFrom(valueType)) return true;
3097
3098 if (IsEnumerableType(targetType) && typeof(IEnumerable).IsAssignableFrom(valueType)) return true;
3099
3100 if (IsClassType(targetType) && typeof(IDictionary).IsAssignableFrom(valueType)) return true;
3101
3102 return false;
3103 }
3104 internal static DataFormat GetDataFormatFromExtension(string filename)
3105 {
3106 switch (Path.GetExtension(filename).ToLowerInvariant().Replace(".", ""))
3107 {
3108 case "csv":
3109 return DataFormat.CSV;
3110 case "tsv":
3111 return DataFormat.TAB;
3112 case "json":
3113 case "jsn":
3114 return DataFormat.JSON;
3115 // lines of text with nothing to split on. .txt used to answer TAB, which split a plain text file
3116 // on tabs it does not have
3117 case "txt":
3118 case "log":
3119 return DataFormat.LOG;
3121 //case "pdf":
3122 // return DataFormat.PDF;
3123 case "prn":
3124 return DataFormat.PRN;
3125 case "xls":
3126 case "xlsx":
3127 return DataFormat.XLSX;
3128 case "xml":
3129 return DataFormat.XML;
3130 case "yml":
3131 case "yaml":
3132 return DataFormat.YAML;
3133 case "htm":
3134 case "html":
3135 return DataFormat.HTML;
3136 default:
3137 return DataFormat.NOTSET;
3138 }
3139 }
3140 // Method to convert a dictionary to an XmlElement recursively
3141 public static void ConvertDictionaryToXmlElement(
3142 dynamic obj,
3143 XmlElement parent,
3144 bool emitWrapper = true,
3145 bool isTopLevel = true,
3146 HashSet<object> visited = null) // cycle detection for reference types
3147 {
3148 if (visited == null) visited = new HashSet<object>(ReferenceEqualityComparer.Instance);
3149
3150 XmlDocument doc = parent.OwnerDocument;
3151
3152 // Top-level emit = false: clear document and let first element become parent
3153 XmlElement currentParent = parent;
3154 if (!emitWrapper && isTopLevel)
3155 {
3156 doc.RemoveAll();
3157 currentParent = null; // no parent yet
3158 }
3159
3160 Type objType = obj?.GetType();
3161 bool isDictionary = obj is IDictionary;
3162 bool isEnumerable = IsEnumerableType(objType) && !(obj is string);
3163 bool isSimple = IsSimpleType(objType);
3164 bool isTuple = IsTupleType(objType);
3165
3166 // Detect cycles in reference types before recursing; exclude string — it's immutable,
3167 // commonly interned, and can never form a real cycle
3168 if (objType != null && !objType.IsValueType && objType != typeof(string) && (object)obj != null)
3169 {
3170 if (!visited.Add((object)obj))
3171 {
3172 if (currentParent != null)
3173 {
3174 var bt = objType.Name.IndexOf('`');
3175 var cycleName = bt >= 0 ? objType.Name.Substring(0, bt) : objType.Name;
3176 currentParent.InnerText = $"Recursion to [{cycleName}]";
3177 }
3178 return;
3179 }
3180 }
3181
3182 // Handle dictionary attributes and #text
3183 if (isDictionary)
3184 {
3185 dynamic dict = obj;
3186 foreach (var kvp in dict)
3187 {
3188 string key = kvp.Key.ToString();
3189 if (key.StartsWith("@GPAL_"))
3190 {
3191 // Assign attributes to currentParent if it exists, otherwise will be assigned after first element
3192 if (currentParent != null)
3193 currentParent.SetAttribute(key.Substring(6), ValueToString(kvp.Value));
3194 }
3195 }
3196 foreach (var kvp in dict)
3197 {
3198 string key = kvp.Key.ToString();
3199 if (key == "#text" && currentParent != null)
3200 {
3201 currentParent.InnerText = ValueToString(kvp.Value);
3202 }
3203 }
3204 }
3205
3206 // Main processing will always start with a dictionary
3207 if (isDictionary)
3208 {
3209 dynamic dict = obj;
3210 foreach (var kvp in dict)
3211 {
3212 string key = kvp.Key.ToString();
3213 if (key.StartsWith("@") || key == "#text") continue;
3214 if (key.StartsWith("GPALKEY")) key = "row";
3215 // ConvertXmlNodeToDictionary tags each child "GPAL###_tagname" purely to keep same-named
3216 // siblings from colliding as dictionary keys before StripGpalKeys groups them - the index
3217 // has no meaning past that point and must never reach output as part of an element name.
3218 else if (System.Text.RegularExpressions.Regex.IsMatch(key, @"^GPAL\d+_"))
3219 key = System.Text.RegularExpressions.Regex.Replace(key, @"^GPAL\d+_", "");
3220
3221 string elementName = SanitizeXmlElementName(key);
3222 dynamic value = kvp.Value;
3223
3224 XmlElement child = doc.CreateElement(elementName);
3225
3226 // If top-level and emit=false, this is the first element
3227 if (currentParent == null)
3228 {
3229 currentParent = child;
3230 doc.AppendChild(child); // first element becomes root
3231 }
3232 else
3233 {
3234 currentParent.AppendChild(child);
3235 }
3236
3237 ConvertDictionaryToXmlElement(value, child, emitWrapper, false, visited);
3238 }
3239 }
3240 else if (isEnumerable)
3241 {
3242 foreach (dynamic item in obj)
3243 {
3244 if (emitWrapper)
3245 {
3246 XmlElement child = doc.CreateElement("item");
3247 currentParent.AppendChild(child);
3248 ConvertDictionaryToXmlElement(item, child, emitWrapper, false, visited);
3249 }
3250 else
3251 {
3252 ConvertDictionaryToXmlElement(item, currentParent, emitWrapper, false, visited);
3253 }
3254 }
3255 }
3256 else if (isTuple)
3257 {
3258 if (currentParent != null)
3259 {
3260 var fields = objType.GetFields();
3261
3262 foreach (var field in fields)
3263 {
3264 dynamic fieldValue = field.GetValue(obj);
3265 XmlElement child = doc.CreateElement(SanitizeXmlElementName(field.Name));
3266 currentParent.AppendChild(child);
3267 ConvertDictionaryToXmlElement(fieldValue, child, emitWrapper: true, isTopLevel: false, visited: visited);
3268 }
3269 }
3270 }
3271 else if (isSimple && (objType == null || !objType.IsValueType || objType.IsPrimitive || objType.IsEnum
3272 || objType == typeof(DateTime) || objType == typeof(DateTimeOffset) || objType == typeof(decimal)
3273 || IsNullableType(objType) || IsGuidType(objType) || IsTimeSpanType(objType) || IsBigIntegerType(objType)
3274 || IsComplexType(objType)))
3275 {
3276 if (currentParent != null)
3277 currentParent.InnerText = ValueToString(obj);
3278 }
3279 else if ((object)obj != null && currentParent != null)
3280 {
3281 // Complex struct or class — reflect public properties and fields into child elements
3282 foreach (var prop in objType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
3283 {
3284 try
3285 {
3286 dynamic val = prop.GetValue(obj);
3287 XmlElement child = doc.CreateElement(SanitizeXmlElementName(prop.Name));
3288 currentParent.AppendChild(child);
3289 ConvertDictionaryToXmlElement(val, child, emitWrapper, false, visited);
3290 }
3291 catch { }
3292 }
3293 foreach (var field in objType.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
3294 {
3295 try
3296 {
3297 dynamic val = field.GetValue(obj);
3298 XmlElement child = doc.CreateElement(SanitizeXmlElementName(field.Name));
3299 currentParent.AppendChild(child);
3300 ConvertDictionaryToXmlElement(val, child, emitWrapper, false, visited);
3301 }
3302 catch { }
3303 }
3304 }
3305 }
3306 public static void ConvertHtmlDictionaryToXml(dynamic obj, XmlElement parent)
3307 {
3308 if (obj == null)
3309 return;
3310
3311 XmlDocument doc = parent.OwnerDocument;
3312
3313 // Case 1: List of children (common for "children" array)
3314 if (obj is IEnumerable enumerable && !(obj is string) && !(obj is IDictionary))
3315 {
3316 foreach (dynamic child in enumerable)
3317 {
3318 ConvertHtmlDictionaryToXml(child, parent);
3319 }
3320 return;
3321 }
3322
3323 // Case 2: Plain text node
3324 if (obj is string text)
3325 {
3326 parent.AppendChild(doc.CreateTextNode(text));
3327 return;
3328 }
3329
3330 // Case 3: HTML comment node — special case for your parser format
3331 if (true == IsDictionaryType(obj.GetType()))
3332 {
3333 object tagObj;
3334 if (obj.TryGetValue("tag", out tagObj) && "#comment".Equals(tagObj?.ToString()))
3335 {
3336 string commentText = "";
3337 object valueObj;
3338 if (obj.TryGetValue("value", out valueObj))
3339 {
3340 commentText = SanitizeComment(ValueToString(valueObj));
3341 }
3342
3343 // Create real XML comment
3344 parent.AppendChild(doc.CreateComment(commentText));
3345 return;
3346 }
3347 }
3348
3349 // Case 4: Regular element node
3350 if (!(obj is IDictionary elementDict))
3351 {
3352 parent.AppendChild(doc.CreateTextNode(ValueToString(obj)));
3353 return;
3354 }
3355
3356 string tagName = "div";
3357 dynamic attributes = null;
3358 dynamic children = null;
3359 string textContent = null;
3360
3361 foreach (DictionaryEntry entry in elementDict)
3362 {
3363 string key = entry.Key.ToString().ToLower();
3364 if (key == "tag")
3365 tagName = entry.Value?.ToString() ?? "div";
3366 else if (key == "attributes")
3367 attributes = entry.Value;
3368 else if (key == "children")
3369 children = entry.Value;
3370 else if (key == "text")
3371 textContent = entry.Value?.ToString();
3372 }
3373
3374 XmlElement element = doc.CreateElement(SanitizeXmlElementName(tagName));
3375
3376 // Attributes
3377 if (attributes is IDictionary attrDict)
3378 {
3379 foreach (DictionaryEntry attr in attrDict)
3380 {
3381 string attrName = attr.Key.ToString();
3382 string attrValue = ValueToString(attr.Value);
3383 element.SetAttribute(attrName, attrValue);
3384 }
3385 }
3386
3387 // Text content
3388 if (!string.IsNullOrEmpty(textContent))
3389 {
3390 element.AppendChild(doc.CreateTextNode(textContent));
3391 }
3392
3393 // Children
3394 if (children != null)
3395 {
3396 ConvertHtmlDictionaryToXml(children, element);
3397 }
3398
3399 parent.AppendChild(element);
3400 }
3401
3402 // Helper to format known complex types nicely
3403 private static string ValueToString(dynamic value, Type type = null)
3404 {
3405 if (null == type)
3406 type = value?.GetType();
3407
3408 // KeyValuePair – return only the Value
3409 if (IsKeyValuePairType(type))
3410 return ValueToString(value.Value);
3411
3412 if (false == IsCustomStructType(type))
3413 try
3414 {
3415 if (null == value || value is char c && c == '\0')
3416 return "null";
3417 }
3418 catch
3419 {
3420 return "null";
3421 }
3422
3423 // Already a string (e.g. round-tripped from YAML/JSON, where Complex/Tuple values
3424 // are stored in their string-formatted form) - nothing further to format.
3425 if (value is string alreadyString)
3426 return alreadyString;
3427
3428 // XML may leave a scalar property's value in a List<object> or dict wrapper that
3429 // StripGpalKeys didn't fully collapse. Extract the inner string safely so the
3430 // type-specific ToString calls below receive a real value, not a container object.
3431 if (value is IList scalarList && type != null && !IsEnumerableType(type) && !type.IsArray)
3432 {
3433 object first = scalarList.Count > 0 ? scalarList[0] : null;
3434 if (first == null) return "null";
3435 if (first is string fs) return fs;
3436 // Inner item is a dict — extract first string value (handles {value:"..."} etc.)
3437 if (first is IDictionary fd)
3438 {
3439 foreach (DictionaryEntry de in fd)
3440 if (de.Value is string dvs) return dvs;
3441 return "null";
3442 }
3443 // Inner item is another list — recurse once more
3444 if (first is IList) return ValueToString(first, type);
3445 return first.ToString();
3446 }
3447
3448 if (IsTupleType(type))
3449 {
3450 var fields = type.GetFields();
3451 var parts = new List<string>();
3452 foreach (var field in fields)
3453 {
3454 var fieldValue = field.GetValue(value);
3455 parts.Add(ValueToString(fieldValue)); // recursive — handles nested tuples, BigInteger, etc.
3456 }
3457 return $"({string.Join(", ", parts)})";
3458 }
3459
3460 // Complex – clean format
3461 if (IsComplexType(type))
3462 {
3463 double im = value.Imaginary;
3464 string sign = im >= 0 ? "+" : "-";
3465 return $"{value.Real}{sign}{Math.Abs(im)}i";
3466 }
3467
3468 // Enum (including flags) – use name if possible, fall back to numeric
3469 if (IsEnumType(type))
3470 {
3471 // IsEnumType answers true for a nullable enum as well, and every Enum call below needs the
3472 // enum itself: Enum.GetName(typeof(Priority?), 2) throws "Type provided must be an Enum",
3473 // which is what a nullable enum member read from JSON hit
3474 Type enumType = Nullable.GetUnderlyingType(type) ?? type;
3475 object realValue = value;
3476
3477 // If incoming value is a string, try to parse it to the target enum
3478 if (value is string strValue)
3479 {
3480 // Use reflection to call generic Enum.TryParse<T>(string, bool, out T)
3481 var tryParseMethod = typeof(Enum).GetMethod(nameof(Enum.TryParse), new[]
3482 {
3483 typeof(string),
3484 typeof(bool),
3485 enumType.MakeByRefType() // out TEnum
3486 });
3487
3488 if (tryParseMethod != null)
3489 {
3490 var parameters = new object[] { strValue, true, null };
3491 bool success = (bool)tryParseMethod.Invoke(null, parameters);
3492
3493 if (success)
3494 {
3495 realValue = parameters[2]; // the out parameter
3496 }
3497 else
3498 {
3499 // String did not match > return as-is
3500 return strValue;
3501 }
3502 }
3503 else
3504 {
3505 dynamic enumObject = InstantiateOne(enumType);
3506
3507 return enumObject.ToString();
3508 // Very old runtime – fallback to case-sensitive non-generic
3509 //if (Enum.TryParse(strValue, out enumObject))
3510 //{
3511 // realValue = enumObject;
3512 //}
3513 //else
3514 //{
3515 // return strValue;
3516 //}
3517 }
3518 }
3519
3520 long numeric = Convert.ToInt64(realValue);
3521
3522 if (numeric == 0)
3523 return "0";
3524
3525 // through Enum.ToObject because JSON hands back an Int64 and GetName wants the enum's own
3526 // underlying type, which for these enums is Int32
3527 string name = Enum.GetName(enumType, Enum.ToObject(enumType, numeric));
3528 if (name != null)
3529 return name;
3530
3531 // Handle flags enum if decorated with FlagsAttribute
3532 if (enumType.IsDefined(typeof(FlagsAttribute), inherit: false))
3533 {
3534 var names = Enum.GetValues(enumType)
3535 .Cast<object>()
3536 .Where(v => Convert.ToInt64(v) != 0 && (numeric & Convert.ToInt64(v)) == Convert.ToInt64(v))
3537 .Select(v => Enum.GetName(enumType, v))
3538 .Where(n => n != null);
3539
3540 if (names.Any())
3541 return string.Join(", ", names);
3542 }
3543
3544 // Fallback: show the numeric value
3545 return numeric.ToString(CultureInfo.InvariantCulture);
3546 }
3547
3548 // DateTime
3549 if (IsDateTimeType(type))
3550 {
3551 return value.ToString("MM/dd/yyyy h:mm:ss tt");
3552 }
3553
3554 // Guid – standard string
3555 if (IsGuidType(type))
3556 {
3557 return value.ToString();
3558 }
3559
3560 // Uri – absolute string
3561 if (IsUriType(type))
3562 {
3563 return value.ToString();
3564 }
3565
3566 // IPAddress – standard string
3567 if (IsIpAddressType(type))
3568 {
3569 return value.ToString();
3570 }
3571
3572 // TimeSpan – standard round-trippable format
3573 if (IsTimeSpanType(type))
3574 {
3575 return value.ToString("c"); // "01:02:03" or "-00:00:01"
3576 }
3577
3578 // Version – standard string
3579 if (IsVersionType(type))
3580 {
3581 return value.ToString();
3582 }
3583
3584 // BigInteger – invariant culture
3585 if (IsBigIntegerType(type))
3586 {
3587 return Convert.ToString(value, CultureInfo.InvariantCulture);
3588 }
3589
3590 // Special float/double values – use standard .NET strings
3591 if (type == typeof(float) || type == typeof(double))
3592 {
3593 if (double.IsNaN((double)value))
3594 return "NaN";
3595 if (double.IsPositiveInfinity((double)value))
3596 return "Infinity";
3597 if (double.IsNegativeInfinity((double)value))
3598 return "-Infinity";
3599
3600 return Convert.ToString(value, CultureInfo.InvariantCulture);
3601 }
3602
3603 if (typeof(WaitTime) == type)
3604 {
3605 WaitTime wt;
3606 if (true == TryConvertToWaitTime(value, out wt))
3607 return wt.ToString();
3608
3609 wt = CreateObjectFromDictionary(value, type, null, null);
3610 return wt.ToString();
3611 }
3612
3613 // Simple built-in types (int, bool, char, decimal, etc.) – invariant culture
3614 if (IsSimpleType(type))
3615 {
3616 return Convert.ToString(value, CultureInfo.InvariantCulture);
3617 }
3618
3619 var toString = GetToString(type);
3620 // Fallback - how'd we get here? a struct? some custome data structure we don't know about
3621 return (string)toString.Invoke(value, null);
3622 }
3623 private static MethodInfo GetToString(Type t)
3624 {
3625 if (!ToStringCache.TryGetValue(t, out var method))
3626 {
3627 method = t.GetMethod(
3628 "ToString",
3629 BindingFlags.Instance | BindingFlags.Public,
3630 binder: null,
3631 types: Type.EmptyTypes,
3632 modifiers: null);
3633
3634 ToStringCache[t] = method;
3635 }
3636
3637 return method;
3638 }
3639
3640 private static string SanitizeComment(string text)
3641 {
3642 if (string.IsNullOrEmpty(text))
3643 return "";
3644
3645 text = text.Replace("<!--", "").Replace("-->", "").Replace("--", "-");
3646
3647 // If it ends with -, add a space
3648 if (text.EndsWith("-"))
3649 text += " ";
3650
3651 // Optional: trim excessive whitespace
3652 return text.Trim();
3653 }
3654 private static string SanitizeXmlElementName(string key)
3655 {
3656 if (string.IsNullOrEmpty(key))
3657 return "item";
3658
3659 if (key.StartsWith("GPALKEY", StringComparison.Ordinal))
3660 key = key.Substring(7);
3661 else if (System.Text.RegularExpressions.Regex.IsMatch(key, @"^GPAL\d+_"))
3662 key = System.Text.RegularExpressions.Regex.Replace(key, @"^GPAL\d+_", "");
3663
3664 // XML element names can't start with digits — prefix with _ to preserve the full value
3665 string sanitized = (key.Length > 0 && !char.IsLetter(key[0]) && key[0] != '_')
3666 ? "_" + key
3667 : key;
3668
3669 // Replace colon (namespace separator) with underscore
3670 sanitized = sanitized.Replace(":", "_");
3671
3672 // Replace any invalid characters with underscore
3673 sanitized = Regex.Replace(sanitized, "[^a-zA-Z0-9_\\-.]", "_");
3674
3675 // If empty after sanitizing (e.g. original was "123" > "" > needs fallback)
3676 if (string.IsNullOrEmpty(sanitized))
3677 return "item";
3678
3679 return sanitized;
3680 }
3681 internal static string ConvertInputDictionaryToDelimitedAndGrid(ConverterSettings converterSettings, out IGPALGrid<string> outGrid)
3682 {
3683 StringBuilder sb = new StringBuilder();
3684 StringBuilder sb2 = new StringBuilder();
3685 IGPALGrid<string> outputGrid = GPAL.Grid.ToGPALObject();
3686 List<string> gridRow = new List<string>();
3687 List<Dictionary<object, dynamic>> rows = new List<Dictionary<object, dynamic>>();
3688 List<string> flattenLastMessage = new List<string>();
3689 bool flattenSupressedMessage = false;
3690 object cleanedInput = ConverterHelper.StripGpalKeys(converterSettings.InputDictionary[0].Values);
3691
3692 // Handle both single root object or list of root objects
3693 IEnumerable<object> rootItems = cleanedInput as IEnumerable<object> ?? new[] { cleanedInput };
3694
3695 // A GPALKEY-surrogate record-list can appear at any nesting depth, not just the root - e.g. a
3696 // dictionary of clients whose per-client value is itself a list of records. StripGpalKeys already
3697 // stripped the "GPALKEY" prefix everywhere, so a record-list now just looks like a dictionary
3698 // whose keys are all plain sequential numbers. Recurse into those instead of handing them to
3699 // Flatten, which would dot/index-prefix every record onto a single, ever-widening row.
3700 bool LooksLikeRecordList(dynamic value) =>
3701 value is IDictionary asDict && asDict.Count > 0 &&
3702 asDict.Keys.Cast<object>().All(k => int.TryParse(k?.ToString(), out _));
3703
3704 void EmitRows(dynamic value)
3705 {
3706 if (LooksLikeRecordList(value))
3707 {
3708 foreach (dynamic subValue in ((IDictionary)value).Values)
3709 EmitRows(subValue);
3710 return;
3711 }
3712
3713 // Some GPALKEY-indexed slots are genuinely empty placeholders (Count == 0) rather than
3714 // real records - skip them instead of emitting a blank CSV line for each one.
3715 if (false == IsCustomStructType(value?.GetType()) && (value == null || (value is ICollection emptyCheck && emptyCheck.Count == 0)))
3716 return;
3717
3718 flattenLastMessage.Clear();
3719 flattenSupressedMessage = false;
3720 var row = new Dictionary<object, dynamic>();
3721
3722 // UnwrapSingleItemLists collapses a single-token row (e.g. ["Selenium"]) down to
3723 // a bare scalar "Selenium". Flatten would key that under "" (empty string), which
3724 // doesn't line up with the "0","1","2"... headers produced by multi-token rows.
3725 // Key it under "0" instead so it lands in the first column.
3726 if (false == IsCustomStructType(value?.GetType()) && null != value && true == IsSimpleType(value.GetType()))
3727 row["0"] = value.ToString();
3728 else
3729 Flatten(value, ref row);
3730
3731 rows.Add(row);
3732 }
3733
3734 foreach (dynamic item in rootItems)
3735 {
3736 EmitRows(item);
3737 }
3738
3739 List<string> headers = null;
3740
3741 // User-supplied column names are positional display labels, not field names, so values are
3742 // looked up by position via canonicalFieldNames rather than by matching header text to a key.
3743 bool useUserSuppliedHeaders = 0 < ((IGPALFileInternal)converterSettings.OutputFile)?.FileSettings.ColumnList.Count() && 0 < ((IGPALFileInternal)converterSettings.OutputFile)?.FileSettings.ColumnList[0].Count(); // CAVEAT: hard coded value, but we are only now allowing one file
3744 // A null-valued field is omitted entirely from a row's dictionary, so different rows can carry
3745 // different subsets/orders of keys. This merges all rows' keys into one order-preserving list,
3746 // anchoring each newly-seen key immediately after its predecessor from the row it came from.
3747 List<string> canonicalFieldNames = null;
3748 if (rows.Any())
3749 {
3750 canonicalFieldNames = new List<string>();
3751 foreach (var row in rows)
3752 {
3753 int lastIndex = -1;
3754 foreach (string key in row.Keys.Cast<string>())
3755 {
3756 int existingIndex = canonicalFieldNames.IndexOf(key);
3757 if (existingIndex >= 0)
3758 {
3759 lastIndex = existingIndex;
3760 }
3761 else
3762 {
3763 canonicalFieldNames.Insert(lastIndex + 1, key);
3764 lastIndex++;
3765 }
3766 }
3767 }
3768 }
3769 if (useUserSuppliedHeaders)
3770 headers = ((IGPALFileInternal)converterSettings.OutputFile).FileSettings.ColumnList[0].ToList();
3771 else
3772 headers = canonicalFieldNames;
3773
3774 if (true == converterSettings.FirstLineIsColumnHeaders && null != headers)
3775 {
3776 foreach (var header in headers)
3777 {
3778 sb2.Append(CreateCSVToken(header.ToString(), converterSettings));
3779 gridRow.Add(header.ToString());
3780 }
3781 if (sb2.Length > 0) sb2.Length--; // Remove trailing comma
3782 sb.AppendLine(sb2.ToString());
3783 sb2.Clear();
3784 outputGrid.AddRow(gridRow);
3785 }
3786
3787 int startIndex = 0;
3788 int headerCount = headers?.Count ?? 0;
3789 // Pivot the data
3790 for (int rowIndex = startIndex; rowIndex < rows.Count; rowIndex++)
3791 {
3792 var dict = rows[rowIndex];
3793 gridRow = new List<string>();
3794
3795 for (int i = 0; i < headerCount; i++)
3796 {
3797 string key = useUserSuppliedHeaders ? (i < canonicalFieldNames.Count ? canonicalFieldNames[i] : null) : headers[i];
3798 string value = key != null && dict.ContainsKey(key) && dict[key] != null ? dict[key].ToString() : "";
3799 sb2.Append(CreateCSVToken(value, converterSettings));
3800 gridRow.Add(value);
3801 }
3802 char delim = true == converterSettings.OutDelimiter.HasValue ? converterSettings.OutDelimiter.Value : ',';
3803
3804 if (sb2.Length > 0 && sb2[sb2.Length - 1] == delim) sb2.Length--; // Remove trailing comma
3805 sb.AppendLine(sb2.ToString());
3806 sb2.Clear();
3807 outputGrid.AddRow(gridRow);
3808 }
3809 // Recursive function to flatten nested dictionary
3810 void Flatten(dynamic dictionaryOrList, ref Dictionary<object, dynamic> outDict, string prefix = "")
3811 {
3812 StringBuilder sb3 = new StringBuilder();
3813 Dictionary<object, dynamic> outDict2 = new Dictionary<object, dynamic>();
3814 Type dictOrListType = dictionaryOrList?.GetType();
3815
3816 if (true == IsDictionaryType(dictOrListType))
3817 {
3818 foreach (dynamic kvp in dictionaryOrList)
3819 {
3820 Type kvpType = kvp.GetType();
3821 dynamic kvpValue = kvp;
3822 dynamic kvpKey = null;
3823
3824 if (true == IsKeyValuePairType(kvpType))
3825 {
3826 kvpValue = kvp.Value;
3827 kvpKey = kvp.Key;
3828 }
3829
3830 Type kvpValueType = kvpValue?.GetType();
3831
3832 if (true == IsComplexType(kvpValueType))
3833 {
3834 Complex complex = (Complex)kvpValue;
3835 sb3.Append("{");
3836 sb3.Append($"{CreateCSVToken(complex.Real.ToString(), converterSettings)}{CreateCSVToken(complex.Imaginary.ToString(), converterSettings)}");
3837// sb3.Append($"{complex.Real.ToString()}{complex.Imaginary.ToString()}");
3838 sb3.Remove(sb3.Length - 1, 1);
3839 sb3.Append("}");
3840 outDict[prefix + kvpKey] = sb3.ToString();
3841 sb3.Clear();
3842 }
3843 else if (true == IsSimpleType(kvpValueType))
3844 {
3845 if (outDict.TryGetValue(prefix + kvpKey, out dynamic val))
3846 sb3.Append($"{val}");
3847 else
3848 sb3.Append(ValueToString(kvpValue));
3849// sb3.Append(CreateCSVToken(kvpValue.ToString(), converterSettings));
3850 outDict[prefix + kvpKey] = sb3.ToString();
3851 sb3.Clear();
3852 }
3853 else if (true == IsIpAddressType(kvpValueType))
3854 {
3855 IPAddress iPAddress = (IPAddress)kvpValue;
3856 sb3.Append(iPAddress.ToString());
3857// sb3.Append(CreateCSVToken(iPAddress.ToString(), converterSettings));
3858 outDict[prefix + kvpKey] = sb3.ToString();
3859 sb3.Clear();
3860 }
3861 else if (IsBigIntegerType(kvpValueType) || IsVersionType(kvpValueType))
3862 {
3863 sb3.Append(kvpValue.ToString());
3864// sb3.Append(CreateCSVToken(kvpValue.ToString(), converterSettings));
3865 outDict[prefix + kvpKey] = sb3.ToString();
3866 sb3.Clear();
3867 }
3868 else if (true == IsDictionaryType(kvpValueType))
3869 {
3870 Flatten(kvpValue, ref outDict2, prefix + kvpKey + ".");
3871 foreach (var item in outDict2)
3872 {
3873 outDict[item.Key] = item.Value;
3874 }
3875 outDict2.Clear();
3876 }
3877 else if ((true == IsEnumerableType(kvpValueType) || true == kvpValueType.IsArray)/* && false == IsStructType(kvpValueType)*/)
3878 {
3879 int index = 0;
3880 foreach (var item in kvpValue)
3881 {
3882 Flatten(item, ref outDict2, $"{prefix}{kvpKey}{index++}.");
3883 foreach (var item2 in outDict2)
3884 {
3885 outDict[item2.Key] = item2.Value;
3886 }
3887 outDict2.Clear(); // clear but DO NOT merge
3888 }
3889 }
3890 else if ((true == IsClassType(kvpValueType)/* || true == IsStructType(kvpValueType)*/) && kvpValueType != typeof(string))
3891 {
3892 foreach (var prop in kvpValueType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
3893 {
3894 try
3895 {
3896 object propValue = prop.GetValue(kvpValue);
3897 if (propValue != null)
3898 {
3899 Flatten(propValue, ref outDict2, $"{prefix}{kvpKey}.{prop.Name}.");
3900 foreach (var item in outDict2)
3901 {
3902 outDict[item.Key] = item.Value;
3903 }
3904 outDict2.Clear();
3905 }
3906 else if (true == IsSimpleType(prop.PropertyType))
3907 {
3908 // A null simple property (e.g. OrganizationName for an individual
3909 // provider record) must still claim its column slot with a blank
3910 // value - silently omitting the key here shifts every subsequent
3911 // property's position out of alignment with a fixed header list.
3912 outDict[$"{prefix}{kvpKey}.{prop.Name}."] = "";
3913 }
3914 }
3915 catch (Exception ex)
3916 {
3917 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to flatten property [{prop.Name}] of [{kvpValueType.Name}]", kvp, GPALObjectType.Other, ex);
3918 }
3919 }
3920 }
3921 else
3922 {
3923 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Unable to handle type [{kvpValueType}]", kvp, GPALObjectType.Other);
3924 }
3925 }
3926 }
3927 else // Top-level input
3928 {
3929 if (true == IsKeyValuePairType(dictOrListType)) // keyvalupair, value is children
3930 {
3931 if (true == IsDictionaryType(dictionaryOrList.Value.GetType()))
3932 {
3933 Flatten(dictionaryOrList.Value, ref outDict2, $"{prefix}");
3934 foreach (var item2 in outDict2)
3935 {
3936 outDict[item2.Key] = item2.Value.Trim(true == converterSettings.InDelimiter.HasValue ? converterSettings.InDelimiter.Value : ',');
3937 }
3938 outDict2.Clear();
3939 }
3940 else
3941 outDict[dictionaryOrList.Key] = dictionaryOrList.Value;
3942 }
3943 // we might be passing in 'null' for a nullable type, so we should output 'null' and return null
3944 else if (false == IsCustomStructType(dictOrListType) && true == ValueToString(dictionaryOrList).Equals("null"))
3945 {
3946 sb3.Append("null");
3947 outDict[prefix] = null;
3948 }
3949 else if (true == IsComplexType(dictOrListType))
3950 {
3951 Complex complex = (Complex)dictionaryOrList;
3952 sb3.Append("{");
3953 sb3.Append($"{complex.Real.ToString()}{complex.Imaginary.ToString()}");
3954 // sb3.Append($"{CreateCSVToken(complex.Real.ToString(), converterSettings)}{CreateCSVToken(complex.Imaginary.ToString(), converterSettings)}");
3955 sb3.Remove(sb3.Length - 1, 1);
3956 sb3.Append("}");
3957 outDict[prefix] = sb3.ToString();
3958 }
3959 else if (true == IsSimpleType(dictOrListType))
3960 {
3961 if (outDict.TryGetValue(prefix, out dynamic val))
3962 sb3.Append($"{val}");
3963 else
3964 sb3.Append(dictionaryOrList.ToString());
3965 // sb3.Append(CreateCSVToken(dictionaryOrList.ToString(), converterSettings));
3966 outDict[prefix] = sb3.ToString();
3967 }
3968 else if (true == IsIpAddressType(dictOrListType))
3969 {
3970 IPAddress iPAddress = (IPAddress)dictionaryOrList;
3971 sb3.Append(iPAddress.ToString());
3972 // sb3.Append(CreateCSVToken(iPAddress.ToString(), converterSettings));
3973 outDict[prefix] = sb3.ToString();
3974 }
3975 else if (IsBigIntegerType(dictOrListType) || IsVersionType(dictOrListType))
3976 {
3977 sb3.Append(dictionaryOrList.ToString());
3978 // sb3.Append(CreateCSVToken(dictionaryOrList.ToString(), converterSettings));
3979 outDict[prefix] = sb3.ToString();
3980 }
3981 else if (true == IsEnumerableType(dictOrListType) || true == IsArrayType(dictOrListType))
3982 {
3983 int index = 0;
3984 foreach (var item in dictionaryOrList)
3985 {
3986 Flatten(item, ref outDict2, $"{prefix}{index++}");
3987 foreach (var item2 in outDict2)
3988 {
3989 outDict[item2.Key] = item2.Value;
3990 }
3991 outDict2.Clear();
3992 }
3993 }
3994 else
3995 {
3996 string flattenMsg = $"Unable to handle type [{dictOrListType}]";
3997 if (false == flattenLastMessage.Contains(flattenMsg))
3998 {
3999 flattenLastMessage.Add(flattenMsg);
4000 GPAL.PublishSimpleEvent(GPALEventType.WARNING, flattenMsg, dictionaryOrList, GPALObjectType.Other);
4001 flattenSupressedMessage = false;
4002 }
4003 else if (false == flattenSupressedMessage)
4004 {
4005 GPAL.PublishSimpleEvent(GPALEventType.INFO, "Supressing repeat messages", dictionaryOrList, GPALObjectType.Other);
4006 flattenSupressedMessage = true;
4007 }
4008 }
4009 }
4010 }
4011
4012 outGrid = outputGrid;
4013 return sb.ToString();
4014 }
4015 internal static string CreateCSVToken(string inputString, ConverterSettings converterSettings)
4016 {
4017 char delimiter = true == converterSettings.OutDelimiter.HasValue ? converterSettings.OutDelimiter.Value : ',';
4018 inputString = (inputString ?? "").TrimEnd(delimiter); // avoid double delimiters. we might be adding these when we parse into the dictionary
4019
4020 StringBuilder sb = new StringBuilder();
4021 if (true == converterSettings.FieldsEnclosedInQuotes)
4022 sb.Append(@"""");
4023
4024 sb.Append(inputString); // NOTE: CAVEAT: is this line required? .Replace(@"""", @""""""));
4025
4026 if (true == converterSettings.FieldsEnclosedInQuotes)
4027 sb.Append(@"""");
4028
4029 if (0 == sb.Length || delimiter != sb[sb.Length - 1])
4030 sb.Append(delimiter);
4031
4032 return sb.ToString();
4033 }
4034 internal static Dictionary<object, dynamic> ConvertXMLToDictionary(XmlDocument xmlDoc)
4035 {
4036 xmlItemCount = 0;
4037 var rootDict = new Dictionary<object, dynamic>();
4038
4039 // Use the actual root element name as the key
4040 string rootName = xmlDoc.DocumentElement.Name; // "catalog"
4041
4042 // Convert the document element (the real root) to its dictionary
4043 dynamic contentDict = ConvertXmlNodeToDictionary(xmlDoc.DocumentElement);
4044
4045 // Put it under the original root name
4046 rootDict[rootName] = contentDict;
4047
4048 return rootDict;
4049 }
4050
4051 private static int xmlItemCount = 0;
4052 private static Dictionary<object, dynamic> ConvertXmlNodeToDictionary(XmlNode xmlNode)
4053 {
4054 var dict = new Dictionary<object, dynamic>();
4055
4056 try
4057 {
4058 // Attributes — prefix with @ so we can distinguish them later
4059 if (xmlNode.Attributes != null)
4060 {
4061 foreach (XmlAttribute attr in xmlNode.Attributes)
4062 {
4063 dict["@GPAL_" + attr.Name] = attr.Value;
4064 }
4065 }
4066
4067 // Group child elements by name (fixes the <row> problem)
4068 var childGroups = new Dictionary<string, List<Dictionary<object, dynamic>>>();
4069
4070 string textContent = null;
4071 string rawWhitespaceText = null; // saved in case this turns out to be a leaf element
4072
4073 foreach (XmlNode childNode in xmlNode.ChildNodes)
4074 {
4075 if (childNode.NodeType == XmlNodeType.Element)
4076 {
4077 var childDict = ConvertXmlNodeToDictionary(childNode);
4078 string name = $"GPAL{xmlItemCount++.ToString("D3")}_"+childNode.Name;
4079
4080 if (!childGroups.TryGetValue(name, out var list))
4081 {
4082 list = new List<Dictionary<object, dynamic>>();
4083 childGroups[name] = list;
4084 }
4085 list.Add(childDict);
4086 }
4087 else if (childNode.NodeType == XmlNodeType.Text
4088 || childNode.NodeType == XmlNodeType.Whitespace
4089 || childNode.NodeType == XmlNodeType.SignificantWhitespace)
4090 {
4091 string raw = childNode.Value ?? string.Empty;
4092 string trimmed = raw.Trim();
4093 if (!string.IsNullOrEmpty(trimmed))
4094 textContent = trimmed;
4095 else if (raw.Length > 0)
4096 rawWhitespaceText = raw; // may be a meaningful space char; keep if leaf
4097 }
4098 }
4099 // Preserve whitespace-only text only in leaf elements (no child elements).
4100 // In parent elements the whitespace is just pretty-print indentation.
4101 if (textContent == null && rawWhitespaceText != null && childGroups.Count == 0)
4102 textContent = rawWhitespaceText;
4103
4104 // Add child elements (single or list)
4105 foreach (var group in childGroups)
4106 {
4107 //if (group.Value.Count == 1)
4108 // dict[group.Key] = group.Value[0];
4109 //else
4110 dict[group.Key] = group.Value;
4111 }
4112
4113 // Add text content with marker
4114 if (textContent != null)
4115 {
4116 dict["#text"] = textContent;
4117 }
4118 }
4119 catch (Exception ex)
4120 {
4121 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to convert XMLnode [{xmlNode}] to dictionary.", null, GPALObjectType.None, ex);
4122 }
4123
4124 return dict;
4125 }
4126 internal static List<Dictionary<object, dynamic>> ConvertGridToDictionary(List<string> columnNames, IGPALGrid<string> rows, int startRow = 0, int rowCnt = -1)
4127 {
4128 List<Dictionary<object, dynamic>> returnList = new List<Dictionary<object, dynamic>>();
4129 var rowDictionary = new Dictionary<object, dynamic>();
4130 int idx = 0;
4131 int ourRowCnt = 0;
4132
4133 if (0 == startRow && -1 == rowCnt)
4134 try
4135 {
4136 foreach (List<string> row in rows)
4137 {
4138 rowDictionary = new Dictionary<object, dynamic>();
4139 idx = 0;
4140
4141 foreach (string columnName in columnNames)
4142 rowDictionary[columnName] = row[idx++];
4143
4144 returnList.Add(rowDictionary);
4145 }
4146 }
4147 catch (Exception ex)
4148 {
4149 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to convert grid to dictionary.", rows, GPALObjectType.Other, ex);
4150 }
4151 else
4152 {
4153 try
4154 {
4155 for (int rowIdx = startRow; ourRowCnt < rowCnt; ourRowCnt++)
4156 {
4157 rowDictionary = new Dictionary<object, dynamic>();
4158 List<string> row = rows[rowIdx++];
4159 idx = 0;
4160
4161 foreach (string columnName in columnNames)
4162 rowDictionary[columnName] = row[idx++];
4163
4164 returnList.Add(rowDictionary);
4165 }
4166 }
4167 catch (Exception ex)
4168 {
4169 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Unable to convert grid to dictionary.", rows, GPALObjectType.Other, ex);
4170 }
4171
4172 }
4173
4174 return returnList;
4175 }
4188 internal static Dictionary<object, dynamic> ConvertDelimitedToDictionary(
4189 ConverterSettings converterSettings,
4190 dynamic inputFile,
4191 string filename = null)
4192 {
4193 var records = new Dictionary<object, dynamic>();
4194 List<string> headers = null;
4195 int lineNumber = 0;
4196
4197 char delimiter =
4198 converterSettings?.InDelimiter ??
4199 GetDelimiterFromFormat(
4200 GetDataFormatFromExtension(filename ?? @"csv")
4201 ) ?? ',';
4202
4203 try
4204 {
4205 TextReader reader;
4206
4207 if (inputFile is Stream stream)
4208 {
4209 stream.Position = 0;
4210 reader = new StreamReader(stream);
4211 }
4212 else if (inputFile is StreamReader sr)
4213 {
4214 reader = sr;
4215 }
4216 else if (inputFile is StringReader str)
4217 {
4218 reader = str;
4219 }
4220 else
4221 {
4223 GPALEventType.ERROR,
4224 $@"Unsupported inputFile type [{inputFile?.GetType().FullName}]",
4225 inputFile,
4226 GPALObjectType.GPALFile);
4227 return records;
4228 }
4229
4230 string line;
4231 while ((line = reader.ReadLine()) != null)
4232 {
4233 if (string.IsNullOrWhiteSpace(line))
4234 {
4235 lineNumber++;
4236 continue;
4237 }
4238
4239 var tokens = SplitRespectingQuotes(line, delimiter);
4240
4241 // First line: header handling
4242 if (lineNumber == 0)
4243 {
4244 if (converterSettings?.FirstLineIsColumnHeaders == true)
4245 {
4246 headers = tokens;
4247 lineNumber++;
4248 continue; // skip header row
4249 }
4250 // No headers — treat first line as data
4251 }
4252
4253 // Row processing
4254 if (headers != null)
4255 {
4256 // With headers > Dictionary<string, string>
4257 var rowDict = new Dictionary<string, string>();
4258 for (int i = 0; i < Math.Min(headers.Count, tokens.Count); i++)
4259 {
4260 rowDict[headers[i]] = tokens[i];
4261 }
4262 records[$"GPALKEY{lineNumber:D4}"] = rowDict;
4263 }
4264 else
4265 {
4266 // No headers > List<string> (raw values, order preserved)
4267 records[$"GPALKEY{lineNumber:D4}"] = tokens;
4268 }
4269
4270 lineNumber++;
4271 }
4272 }
4273 catch (Exception ex)
4274 {
4276 GPALEventType.EXCEPTION,
4277 $@"Exception while parsing delimited file",
4278 inputFile,
4279 GPALObjectType.GPALFile, ex);
4280 }
4281
4282 if (records.Count == 0)
4283 {
4285 GPALEventType.INFO,
4286 $@"No data found in input file [{filename}]",
4287 inputFile,
4288 GPALObjectType.GPALFile);
4289 }
4290
4291 return records;
4292 }
4293
4300 private static IEnumerable<string> ReadAllLines(TextReader reader)
4301 {
4302 var lines = new List<string>();
4303 string line;
4304
4305 // Read until end of stream
4306 while (null != (line = reader.ReadLine()))
4307 {
4308 lines.Add(line);
4309 }
4310
4311 return lines;
4312 }
4313 internal static char? GetDelimiterFromFormat(DataFormat dataFormat)
4314 {
4315 switch (dataFormat)
4316 {
4317 case DataFormat.CARET:
4318 return ('^');
4319 case DataFormat.COLON:
4320 return (':');
4321 case DataFormat.CSV:
4322 return (',');
4323 case DataFormat.DOT:
4324 return ('.');
4325 case DataFormat.HYPHEN:
4326 return ('-');
4327 case DataFormat.PIPE:
4328 return ('|');
4329 case DataFormat.PRN:
4330 case DataFormat.SPACE:
4331 return (' ');
4332 case DataFormat.SEMICOLON:
4333 return (';');
4334 case DataFormat.TAB:
4335 return ('\t');
4336
4337 case DataFormat.HTML:
4338 case DataFormat.JSON:
4340 case DataFormat.XLSX:
4341 case DataFormat.XML:
4342 case DataFormat.YAML:
4343 return null;
4344
4345 case DataFormat.CUSTOM_DELIMITER:
4346 return ConverterSettings.InDelimiter.Value;
4347
4348 default:
4349 //GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to determine delimiter from DataFormat [{dataFormat}], using comma.", null, GPALObjectType.None);
4350 return (',');
4351 }
4352 }
4353
4354 // Check if dict has sequential GPALKEY# keys like an array
4355 internal static bool IsArrayLike(dynamic dictListOrObject)
4356 {
4357 Type parmType = dictListOrObject.GetType();
4358
4359 if (IsDictionaryType(parmType) || IsEnumerableType(parmType))
4360 {
4361 if (dictListOrObject == null || dictListOrObject.Count == 0) return false;
4362
4363 foreach (dynamic entry in dictListOrObject)
4364 {
4365 try
4366 {
4367 string keyStr = entry.Key.ToString();
4368 if (!keyStr.StartsWith("GPALKEY")) return false; // All keys must be surrogate
4369 if (int.TryParse(keyStr.Substring(7), out int idx))
4370 break;
4371 else
4372 return false;
4373 }
4374 catch
4375 {
4376 return false;
4377 }
4378 }
4379 }
4380 else
4381 return false;
4382
4383 return true;
4384 }
4385
4386 // Extract id and return rest of dict if id exists
4387 private static bool TryGetIdAndRest(IDictionary dict, out string id, out Dictionary<object, dynamic> rest)
4388 {
4389 id = string.Empty;
4390 rest = new Dictionary<object, dynamic>();
4391
4392 if (dict.Contains("id"))
4393 {
4394 dynamic idValue = dict["id"];
4395 if (idValue is string idStr)
4396 {
4397 id = idStr;
4398 foreach (DictionaryEntry kvp in dict)
4399 {
4400 if (!"id".Equals(kvp.Key.ToString()))
4401 {
4402 rest[kvp.Key] = kvp.Value;
4403 }
4404 }
4405 return true;
4406 }
4407 }
4408
4409 // Fallback: copy all
4410 foreach (DictionaryEntry kvp in dict)
4411 {
4412 rest[kvp.Key] = kvp.Value;
4413 }
4414 return false;
4415 }
4416
4417 // Render full object
4418 private static string RenderFullObjectToHtml(IDictionary dict, bool emitUl = false, bool isTopLevel = true, string listStyle = "style='margin: 8px; padding-left: 8px;'")
4419 {
4420 var sb = new StringBuilder();
4421 int dictCount = dict.Count;
4422
4423 if (true == isTopLevel)
4424 sb.Append($"<ul id='renderfull' {listStyle}>\n");
4425
4426 foreach (object keyObj in dict.Keys)
4427 {
4428 string key = keyObj.ToString().Replace("@GPAL_", "");
4429
4430 key = System.Text.RegularExpressions.Regex.Replace(key, @"^GPAL\d+_", "");
4431
4432 // GPALKEY#### stands in for an array index, it is not a field name. Blanking it takes the same
4433 // route the empty-key case below already takes, which is what the comment there means by not
4434 // emitting our array index keys
4435 if (true == System.Text.RegularExpressions.Regex.IsMatch(key, @"^GPALKEY\d+$"))
4436 key = string.Empty;
4437
4438 if ("#comment".Equals(key)) continue;
4439 dynamic value = dict[keyObj];
4440
4441 if (false == key.Equals("#text") && false == string.IsNullOrEmpty(key)) // do not emit our array index keys
4442 {
4443 sb.Append("<li><strong>");
4444 sb.Append(HttpUtility.HtmlEncode(key));
4445 sb.Append("</strong>: ");
4446 listStyle = "style = 'list-style: none; margin: 8px; padding-left: 8px;'";
4447 }
4448 Type valueType = value?.GetType();
4449 bool isCollection = IsDictionaryType(valueType) || IsEnumerableType(valueType);
4450 bool isTuple = IsTupleType(valueType);
4451
4452 int collectionCount = 0;
4453
4454 if (true == isCollection)
4455 collectionCount = ObjectCopier.GetItemCount(null, value);
4456
4457 if (true == emitUl && 1 < dictCount)
4458 sb.Append($"<ul id=rf2 {listStyle}><li>\n");
4459
4460 sb.Append(RenderValue(value, isCollection || isTuple, isCollection || isTuple ? "style='margin: 8px; padding-left: 8px;'" : listStyle));
4461
4462 if (true == emitUl && 1 < dictCount)
4463 sb.Append("</ul></li>");
4464
4465 if (false == key.Equals("#text"))
4466 sb.Append("</li>\n");
4467 }
4468
4469 if (true == isTopLevel/* || true == emitUl*/)
4470 sb.Append("</ul>\n");
4471
4472 return sb.ToString();
4473 }
4474
4475 private static int idCnt = 0;
4476
4477 // Render any value recursively
4478 private static string RenderValue(dynamic value, bool renderUl = false, string ulStyle = "style='margin: 0px; padding-left: 0px;", HashSet<object> visited = null)
4479 {
4480 var valueSb = new StringBuilder();
4481 Type valueType = value?.GetType();
4482 string vtn = valueType?.Name;
4483
4484 // Initialize visited set at root call
4485 bool isRootCall = visited == null;
4486 bool isNull = false;
4487
4488 if (isRootCall)
4489 {
4490 visited = new HashSet<object>(ReferenceEqualityComparer.Instance); // Critical: reference equality
4491 }
4492
4493 try
4494 {
4495 isNull = null == value;
4496 }
4497 catch
4498 {
4499 isNull = false; // we have a struct or some value we cannot compare to null that will never be null
4500 }
4501
4502 Dictionary<object, dynamic> dict = new Dictionary<object, dynamic>();
4503
4504 if (false == isNull)
4505 {
4506
4507 // === CYCLE DETECTION: If this is a reference type we've already seen, return a marker ===
4508 if (valueType.IsClass || valueType.IsInterface) // Reference types only
4509 {
4510 if (visited.TryGetValue(value, out dynamic fuggetAboutIt))
4511 {
4512 string referenceName = string.Empty;
4513
4514 try
4515 {
4516 referenceName = value.Value;
4517 }
4518 catch
4519 {
4520 try
4521 {
4522 referenceName = value.Name;
4523 }
4524 catch
4525 {
4526 referenceName = valueType.Name;
4527 }
4528 }
4529 // We've seen this exact object before > return a reference marker
4530 return $"Recursion to [{referenceName}]";
4531 }
4532
4533 // First time seeing this object > add to visited
4534 visited.Add(value);
4535 }
4536 }
4537
4538 if (true == IsKeyValuePairType(valueType))
4539 {
4540 return $"<strong>{value.Key}</strong>: {RenderValue(value.Value, true, ulStyle, visited)}";
4541 }
4542 // Tuple (special case)
4543 else if (IsTupleType(valueType))
4544 {
4545 if (true == renderUl)
4546 valueSb.Append($"<ul id=rvtuple{idCnt++} {ulStyle}'>\n");
4547
4548 // Render tuple in a clean, readable way
4549 var fields = valueType.GetFields();
4550 var parts = new List<string>();
4551 foreach (dynamic field in fields)
4552 {
4553 var fieldValue = field.GetValue(value);
4554 valueSb.AppendLine($"<li><strong>{field.Name}</strong>: {RenderValue(fieldValue, true, ulStyle, visited)}</li>");
4555 }
4556
4557 if (true == renderUl)
4558 valueSb.Append("</ul>\n");
4559 }
4560 else if (false == IsCustomStructType(valueType) && value == null)
4561 return HttpUtility.HtmlEncode("null");
4562 // Nested Dictionary<object, dynamic> -> full recursive call
4563 else if (value is Dictionary<object, dynamic> nestedDict)
4564 {
4565 valueSb.Append(RenderFullObjectToHtml(nestedDict, renderUl, false));
4566 }
4567 // IEnumerable (list/array)
4568 else if (true == IsEnumerableObject(value) && !(value is string))
4569 {
4570 if (true == renderUl)
4571 valueSb.Append($"<ul id=rvenum{idCnt++} {ulStyle}'>\n");
4572
4573 foreach (var item in value)
4574 {
4575 Type itemType = item?.GetType();
4576 bool isCollection = IsDictionaryType(itemType) || IsEnumerableType(itemType);
4577 string toRender = RenderValue(item, isCollection || renderUl, ulStyle, visited);
4578
4579 if (false == string.IsNullOrEmpty(toRender))
4580 {
4581 // if we are a collection, bullets will be added, do not add here
4582 // however if it is a scalar, add a bullet here
4583 if ((false == isCollection || true == renderUl) && false == toRender.StartsWith("<li>")) // a bit of a kludge, at times we get <li><li>
4584 valueSb.Append("<li>");
4585
4586 valueSb.Append(toRender);
4587
4588 if ((false == isCollection || true == renderUl) && false == toRender.StartsWith("<li>")) // a bit of a kludge, at times we get <li><li>
4589 valueSb.Append("</li>\n");
4590 }
4591 }
4592
4593 if (true == renderUl)
4594 valueSb.Append("</ul>\n");
4595 }
4596 // Primitive value
4597 // Complex struct (any value type that's not simple and not tuple)
4598 else if (true == IsSimpleType(valueType) && false == IsCustomStructType(valueType))
4599 {
4600 string toRender = ValueToString(value);
4601 if (false == string.IsNullOrEmpty(toRender))
4602 valueSb.Append($"{HttpUtility.HtmlEncode(toRender)}");
4603 }
4604 else if (true == IsCustomStructType(valueType) || true == IsClassType(valueType))
4605 {
4606 if (true == renderUl)
4607 valueSb.Append($"<ul id=rvstruct{idCnt++} {ulStyle}'>\n");
4608
4609 var fields = valueType.GetFields(BindingFlags.Public | BindingFlags.Instance);
4610 var props = valueType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
4611
4612 foreach (var field in fields)
4613 {
4614 dynamic fieldValue = field?.GetValue(value);
4615 valueSb.Append($"<li><strong>{field?.Name}</strong>: ");
4616 Type fieldValueType = fieldValue?.GetType();
4617
4618 if (true == IsEnumerableType(fieldValueType))
4619 valueSb.Append(RenderValue(fieldValue, true, ulStyle, visited));
4620 else
4621 valueSb.Append(ValueToString(fieldValue));
4622
4623 valueSb.Append("</li>\n");
4624 }
4625
4626 foreach (var prop in props)
4627 {
4628 if (true == prop?.CanRead && prop?.GetIndexParameters().Length == 0)
4629 {
4630 dynamic propValue = prop?.GetValue(value);
4631 valueSb.Append($"<li><strong>{prop?.Name}</strong>: ");
4632 Type propValueType = propValue?.GetType();
4633 bool isEnumerable = IsEnumerableType(propValueType);
4634 bool isClass = IsClassType(propValueType);
4635 bool isSimple = IsSimpleType(propValueType);
4636
4637 if (true == isEnumerable || (true == isClass && false == isSimple))
4638 valueSb.Append(RenderValue(propValue, true, ulStyle, visited));
4639 else
4640 valueSb.Append(ValueToString(propValue));
4641
4642 valueSb.Append("</li>\n");
4643 }
4644 }
4645
4646 if (true == renderUl)
4647 valueSb.Append("</ul>\n");
4648 }
4649 else
4650 {
4651 valueSb.Append(ValueToString(value));
4652 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Render [{valueType}] using ValueToString", value, GPALObjectType.Other);
4653 }
4654
4655 return valueSb.ToString();
4656 }
4657
4658 // Main method to convert dictionary to HTML
4659 public static string ConvertDictionaryToHtml(Dictionary<object, dynamic> dictionary)
4660 {
4661 var sb = new StringBuilder();
4662
4663 if (0 == dictionary.Count)
4664 {
4665 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "Dictionary is empty. No html will be output.", dictionary, GPALObjectType.Converter);
4666 return string.Empty;
4667 }
4668
4669 if (dictionary.ContainsKey("tag"))
4670 {
4671 var tagName = dictionary["tag"].ToString();
4672 if ("#comment".Equals(tagName))
4673 {
4674 if (dictionary.ContainsKey("value"))
4675 {
4676 sb.Append($" {dictionary["value"].ToString()}\n");
4677 }
4678 return sb.ToString();
4679 }
4680
4681 if (false == "#text".Equals(tagName))
4682 {
4683 sb.Append($"<{tagName}");
4684 if (dictionary.ContainsKey("attributes"))
4685 {
4686 dynamic attributes = dictionary["attributes"];
4687 foreach (var attribute in attributes)
4688 {
4689 sb.Append($" {attribute.Key}=\"{attribute.Value}\"");
4690 }
4691 }
4692
4693 switch (tagName)
4694 {
4695 case "br":
4696 case "hr":
4697 sb.Append("/>\n");
4698 break;
4699 case "img":
4700 case "input":
4701 case "meta":
4702 case "link":
4703 case "html":
4704 case "head":
4705 case "body":
4706 sb.Append(">\n");
4707 break;
4708 default:
4709 sb.Append(">");
4710 break;
4711 }
4712 }
4713
4714 if (dictionary.ContainsKey("text"))
4715 {
4716 sb.Append(dictionary["text"].ToString());
4717 }
4718
4719 if (dictionary.ContainsKey("children"))
4720 {
4721 var children = (List<Dictionary<object, dynamic>>)dictionary["children"];
4722 foreach (var child in children)
4723 {
4724 if (false == child.ContainsKey("#text"))
4725 {
4726 string text = ConvertDictionaryToHtml(child);
4727 if (0 < text.Length)
4728 sb.Append(text);
4729 }
4730 else
4731 sb.Append(child["text"]);
4732 }
4733 }
4734
4735 switch (tagName)
4736 {
4737 case "br":
4738 case "hr":
4739 case "meta":
4740 case "img":
4741 case "input":
4742 case "link":
4743 case "#text":
4744 break;
4745 default:
4746 sb.Append($"</{tagName}>\n");
4747 break;
4748 }
4749 }
4750 else
4751 {
4752 // Generic data rendering (no "tag")
4753 bool isArray = IsArrayLike(dictionary);
4754 if (isArray)
4755 {
4756 sb.Append($"<ul id=list style='margin: 8px; padding-left: 8px;'>\n");
4757
4758 foreach (var kvp in dictionary)
4759 {
4760 dynamic item = kvp.Value;
4761 string toRender = RenderValue(item, true, "style='margin: 8px; padding-left: 8px;");
4762
4763 if (false == string.IsNullOrEmpty(toRender))
4764 {
4765 sb.Append("<li>");
4766 sb.Append(toRender);
4767 sb.Append("</li>\n");
4768 }
4769 }
4770
4771 sb.Append("</ul>");
4772 }
4773 else
4774 {
4775 sb.Append(RenderFullObjectToHtml(dictionary));
4776 }
4777 }
4778 return sb.ToString();
4779 }
4780
4787 public static List<Dictionary<object, dynamic>> ConvertHtmlToDictionary(dynamic inputDataOrFilename, bool isChildren = false)
4788 {
4789 var dictionaries = new List<Dictionary<object, dynamic>>();
4790 HtmlNodeCollection nodes = null;
4791
4792 if (inputDataOrFilename is HtmlNodeCollection)
4793 nodes = inputDataOrFilename;
4794 else
4795 nodes = GetHtmlNodes(inputDataOrFilename);
4796
4797 if (null == nodes)
4798 {
4799 GPAL.PublishSimpleEvent(GPALEventType.WARNING, "No html nodes found. Dictionary will be empty", inputDataOrFilename, GPALObjectType.Converter);
4800 return new List<Dictionary<object, dynamic>>();
4801 }
4802
4803 foreach (var node in nodes)
4804 {
4805 // Skip only irrelevant root-level nodes like #document
4806 if (node.Name == "#document" && false == isChildren)
4807 continue;
4808
4809 // Handle #comment nodes explicitly
4810 if (true == "#comment".Equals(node.Name))
4811 {
4812 if (false == isChildren)
4813 continue;
4814
4815 var commentDict = new Dictionary<object, dynamic>
4816 {
4817 { "tag", "#comment" },
4818 { "value", node.InnerHtml }
4819 };
4820 dictionaries.Add(commentDict);
4821 continue;
4822 }
4823
4824 // Handle #text nodes
4825 if (true == "#text".Equals(node.Name))
4826 {
4827 if (false == isChildren)
4828 continue;
4829
4830 var textDict = new Dictionary<object, dynamic>
4831 {
4832 { "tag", "#text" },
4833 { "value", node.InnerHtml }
4834 };
4835 dictionaries.Add(textDict);
4836 continue;
4837 }
4838
4839 var dict = new Dictionary<object, dynamic>();
4840
4841 // Add the tag name and attributes to the dictionary
4842 dict["tag"] = node.Name;
4843 if (node.Attributes.Count > 0)
4844 {
4845 var attributes = new Dictionary<string, string>();
4846 foreach (var attribute in node.Attributes)
4847 {
4848 attributes[attribute.Name] = attribute.Value;
4849 }
4850 dict["attributes"] = attributes;
4851 }
4852
4853 // Handle the node's text content directly
4854 if (!string.IsNullOrEmpty(node.InnerText.Trim()) && !node.HasChildNodes)
4855 {
4856 dict["text"] = node.InnerText.Trim();
4857 }
4858
4859 // Recursively add child nodes to the dictionary
4860 if (node.HasChildNodes)
4861 {
4862 var children = new List<Dictionary<object, dynamic>>();
4863 foreach (var childNode in node.ChildNodes)
4864 {
4865 if (childNode.Name == "#text")
4866 {
4867 if (!string.IsNullOrEmpty(childNode.InnerHtml.Trim()))
4868 {
4869 // If the parent has only text children, store it directly in "text"
4870 if (node.ChildNodes.All(n => n.Name == "#text"))
4871 {
4872 dict["text"] = childNode.InnerHtml.Trim();
4873 break;
4874 }
4875 else
4876 {
4877 children.Add(new Dictionary<object, dynamic>
4878 {
4879 { "tag", "#text" },
4880 { "value", childNode.InnerHtml }
4881 });
4882 }
4883 }
4884 }
4885 else
4886 {
4887 var childDictionaries = ConvertHtmlToDictionary(new HtmlNodeCollection(null) { childNode }, true);
4888 if (childDictionaries.Count > 0)
4889 {
4890 foreach (var childDict in childDictionaries)
4891 {
4892 children.Add(childDict);
4893 }
4894 }
4895 }
4896 }
4897 if (children.Count > 0)
4898 dict["children"] = children;
4899 }
4900
4901 dictionaries.Add(dict);
4902 }
4903 return dictionaries;
4904 }
4905 public static HtmlNodeCollection GetHtmlNodes(dynamic htmlorFilename)
4906 {
4907 string htmlString;
4908 var doc = new HtmlAgilityPack.HtmlDocument();
4909 try
4910 {
4911 htmlString = File.ReadAllText(htmlorFilename);
4912 }
4913 catch
4914 {
4915 htmlString = htmlorFilename;
4916 }
4917
4918 try
4919 {
4920 doc.LoadHtml(htmlString);
4921 return doc.DocumentNode.ChildNodes; // descandants causes duplicates, this works perfectly
4922 }
4923 catch (Exception ex)
4924 {
4925 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Expected html string or GPALFile, got [{htmlorFilename.GetType()}]", htmlorFilename, GPALObjectType.Other, ex);
4926 return null;
4927 }
4928
4929 }
4930 public static dynamic CreateIEnumerableFromClass<T>(ConverterSettings converterSettings)
4931 {
4932 IEnumerable<T> resultList = null;
4933 bool isArrayOutput = converterSettings.OutputClassType.IsArray;
4934 T[] arrayOutput = null;
4935 int arrayCapacity = 0;
4936 int elementIndex = 0;
4937
4938 // Determine capacity for array based on target size, not input
4939 if (isArrayOutput)
4940 {
4941 arrayCapacity = ((Array)Activator.CreateInstance(converterSettings.OutputClassType, 4)).Length; // Use the predefined length (4)
4942 }
4943 else
4944 {
4945 arrayCapacity = converterSettings.InputDictionary.Count();
4946 }
4947
4948 // Instantiate the output collection
4949 if (isArrayOutput)
4950 {
4951 arrayOutput = (T[])Array.CreateInstance(converterSettings.OutputClassType.GetElementType(), arrayCapacity);
4952 // Populate with empty instances up to array length
4953 for (int i = 0; i < arrayCapacity; i++)
4954 {
4955 arrayOutput[i] = (T)InstantiateOne(converterSettings.OutputClassElementType);
4956 }
4957 }
4958 else
4959 {
4960 switch (converterSettings.OutputClassType)
4961 {
4962 default:
4963 resultList = (IEnumerable<T>)ActivatorHelper.CreateInstance(converterSettings.OutputClassType);
4964 break;
4965 }
4966 }
4967
4968 // Process input dictionary, but limit to array capacity if applicable
4969 int inputIndex = 0;
4970 foreach (var dict in converterSettings.InputDictionary)
4971 {
4972 if (isArrayOutput && inputIndex >= arrayCapacity)
4973 {
4974 break; // Stop if we've reached the array capacity
4975 }
4976
4977 bool allKeysSame = false;
4978 try
4979 {
4980 allKeysSame = CheckKeysPrefix(dict);
4981 }
4982 catch (Exception ex)
4983 {
4984 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"CheckKeysPrefix exception", converterSettings, GPALObjectType.Other, ex);
4985 }
4986
4987 int dictIdx = 0;
4988 int dictCount = ObjectCopier.GetItemCount(null, dict);
4989
4990 foreach (var dict2 in dict)
4991 {
4992 T element = (T)(object)InstantiateOne(converterSettings.OutputClassElementType);
4993
4994 dynamic retVal = null;
4995 if (true == allKeysSame)
4996 retVal = ConvertToClass(dict, converterSettings.OutputClassType, dict, null);
4997 else
4998 retVal = ConvertToClass(dict2.Value, converterSettings.OutputClassElementType, dict, null);
4999
5000 if (isArrayOutput)
5001 {
5002 int count = ObjectCopier.GetItemCount(null, retVal);
5003
5004 try
5005 {
5006 if (0 < count)
5007 foreach (dynamic element2 in retVal)
5008 {
5009 arrayOutput[elementIndex++] = (T)element2;
5010 }
5011 else
5012 arrayOutput[elementIndex++] = (T)retVal;
5013 }
5014 catch (IndexOutOfRangeException)
5015 {
5016 GPAL.PublishSimpleEvent(GPALEventType.INFO, $"[{converterSettings.OutputClassType.Name}] does not have enough capacity for [{dictCount}] items. Returning [{elementIndex - 1}]", retVal, GPALObjectType.Other);
5017 break;
5018 }
5019 }
5020 else if (converterSettings.OutputClassType == retVal.GetType())
5021 return retVal;
5022 else
5023 {
5024 element = (T)retVal;
5025
5026 try
5027 {
5028
5029 {
5030 MethodInfo addMethod = ConverterSettings.OutputClassType.GetMethod("Add",
5031 BindingFlags.Public | BindingFlags.Instance, null, new[] { ConverterSettings.OutputClassElementType }, null);
5032 MethodInfo addRowMethod = ConverterSettings.OutputClassType.GetMethod("AddRow",
5033 BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(List<>).MakeGenericType(ConverterSettings.OutputClassElementType) }, null);
5034 MethodInfo enqueueMethod = ConverterSettings.OutputClassType.GetMethod("Enqueue",
5035 BindingFlags.Public | BindingFlags.Instance, null, new[] { ConverterSettings.OutputClassElementType }, null);
5036 MethodInfo pushMethod = ConverterSettings.OutputClassType.GetMethod("Push",
5037 BindingFlags.Public | BindingFlags.Instance, null, new[] { ConverterSettings.OutputClassElementType }, null);
5038
5039 if (null != addRowMethod)
5040 {
5041 converterSettings.OutputData = converterSettings.InputData[dictIdx];
5042 if (null != converterSettings.InDelimiter && null != converterSettings.OutDelimiter)
5043 converterSettings.OutputData = converterSettings.InputData[dictIdx].Replace(converterSettings.InDelimiter.Value, converterSettings.OutDelimiter.Value);
5044 string[] lines = converterSettings.OutputData.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
5045
5046 foreach (object line in lines)
5047 {
5048 addRowMethod.Invoke(resultList, new object[] { new List<object>() { (dynamic)line } });
5049 }
5050 }
5051 else if (null != addMethod)
5052 addMethod.Invoke(resultList, new object[] { element });
5053 else if (null != enqueueMethod)
5054 enqueueMethod.Invoke(resultList, new object[] { element });
5055 else if (null != pushMethod)
5056 pushMethod.Invoke(resultList, new object[] { element });
5057 else
5058 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Can't find 'Add/Enqueue/Push' method for [{converterSettings.OutputClassType}]", converterSettings, GPALObjectType.Other);
5059 }
5060 }
5061 catch (Exception ex)
5062 {
5063 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Can't 'Add' to [{converterSettings.OutputClassType}]", converterSettings, GPALObjectType.Other, ex);
5064 }
5065
5066 if (false == allKeysSame)
5067 break;
5068 }
5069 dictIdx++;
5070 }
5071 inputIndex++;
5072 }
5073
5074 return isArrayOutput ? arrayOutput : resultList;
5075 }
5076 public static List<T> CreateListFromClass<T>(ConverterSettings converterSettings)
5077 {
5078 List<T> resultList = new List<T>();
5079
5080 foreach (var data in converterSettings.InputDictionary) // list<selectoset>
5081 {
5082 dynamic element = InstantiateOne(converterSettings.OutputClassElementType);
5083 element = ConvertToClass(new List<Dictionary<object, dynamic>>() { data }, converterSettings.OutputClassElementType, converterSettings.InputDictionary, null);
5084 // LoadClassFromDictionary<T>(element, data);
5085 resultList.Add(element);
5086 }
5087 return resultList;
5088 }
5089 public static dynamic InstantiateOne(Type objectType, int arrayLength = 0)
5090 {
5091 if (objectType == null)
5092 {
5093 GPAL.PublishSimpleEvent(GPALEventType.ERROR, "ObjectType is null in InstantiateOne", null, GPALObjectType.Other);
5094 return null;
5095 }
5096
5097 if (objectType.IsArray)
5098 {
5099 Type elementType = objectType.GetElementType();
5100 Array dynamicArray = Array.CreateInstance(elementType, arrayLength);
5101 for (int i = 0; i < arrayLength; i++)
5102 {
5103 dynamicArray.SetValue(InstantiateOne(elementType), i);
5104 }
5105 return dynamicArray;
5106 }
5107 else if (objectType == typeof(string))
5108 {
5109 return string.Empty;
5110 }
5111 else if (objectType.IsValueType)
5112 {
5113 return ActivatorHelper.CreateInstance(objectType);
5114 }
5115 else if (IsAnonymousType(objectType))
5116 {
5117 return new System.Dynamic.ExpandoObject();
5118 }
5119 else if (ConverterHelper.IsDictionaryType(objectType))
5120 {
5121 try
5122 {
5123 // First: Try to create the EXACT requested type
5124 return ActivatorHelper.CreateInstance(objectType);
5125 }
5126 catch (Exception exInner)
5127 {
5128 // If that fails (e.g., no parameterless ctor, abstract, interface),
5129 // fall back to creating a concrete Dictionary<,>
5130 try
5131 {
5132 Type[] genericArgs = objectType.IsGenericType
5133 ? objectType.GetGenericArguments()
5134 : new[] { typeof(object), typeof(object) };
5135
5136 Type keyType = genericArgs[0];
5137 Type valueType = genericArgs.Length > 1 ? genericArgs[1] : typeof(object);
5138
5139 Type concreteDictType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
5140 return ActivatorHelper.CreateInstance(concreteDictType);
5141 }
5142 catch (Exception ex)
5143 {
5144 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
5145 $"Failed to instantiate dictionary type [{objectType}] (fallback also failed: [{exInner.Message}])",
5146 null, GPALObjectType.Other, ex);
5147 return new System.Dynamic.ExpandoObject();
5148 }
5149 }
5150 }
5151 else
5152 {
5153 try
5154 {
5155 // Try parameterless constructor first
5156 return ActivatorHelper.CreateInstance(objectType);
5157 }
5158 catch (MissingMethodException)
5159 {
5160 // Try constructor with default parameters
5161 var constructors = objectType.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
5162 foreach (var ctor in constructors)
5163 {
5164 var parameters = ctor.GetParameters();
5165 var args = new object[parameters.Length];
5166 for (int i = 0; i < parameters.Length; i++)
5167 {
5168 var paramType = parameters[i].ParameterType;
5169 if (paramType == typeof(string))
5170 args[i] = string.Empty;
5171 else if (paramType == typeof(int))
5172 args[i] = 0;
5173 else if (paramType == typeof(Dictionary<string, string>))
5174 args[i] = new Dictionary<string, string>();
5175 else if (paramType.IsValueType)
5176 args[i] = ActivatorHelper.CreateInstance(paramType);
5177 else
5178 args[i] = null;
5179 }
5180 try
5181 {
5182 return ctor.Invoke(args);
5183 }
5184 catch
5185 {
5186 // Try next constructor
5187 }
5188 }
5189 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"No suitable constructor for type [{objectType}]", null, GPALObjectType.Other);
5190 return null;
5191 }
5192 }
5193 }
5194
5195 #region <Helpers>
5196 private static bool IsAnonymousType(Type type)
5197 {
5198 return Attribute.IsDefined(type, typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), false) &&
5199 type.IsGenericType &&
5200 type.Name.Contains("Anonymous") &&
5201 (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic;
5202 }
5203
5204 private static Complex ComplexFromDict(dynamic value)
5205 {
5206 // Unwrap single-item list (XML wraps every element in a list)
5207 dynamic dict = value;
5208 if (value is IEnumerable && !(value is IDictionary) && !(value is string))
5209 foreach (var item in (IEnumerable)value) { dict = item; break; }
5210
5211 double real = 0, imaginary = 0;
5212 if (dict is IDictionary idict)
5213 {
5214 foreach (DictionaryEntry entry in idict)
5215 {
5216 string rawEntryKey = entry.Key?.ToString() ?? string.Empty;
5217 string key = rawEntryKey.StartsWith("GPALKEY", StringComparison.Ordinal)
5218 ? rawEntryKey.Substring(7)
5219 : (System.Text.RegularExpressions.Regex.IsMatch(rawEntryKey, @"^GPAL\d+_")
5220 ? System.Text.RegularExpressions.Regex.Replace(rawEntryKey, @"^GPAL\d+_", "")
5221 : rawEntryKey);
5222
5223 // Unwrap single-item list value (XML text nodes arrive as lists)
5224 object val = entry.Value;
5225 if (val is IEnumerable valEnum && !(val is string))
5226 foreach (var v in valEnum) { val = v; break; }
5227
5228 string str = val?.ToString();
5229 if (key.Equals("Real", StringComparison.OrdinalIgnoreCase))
5230 double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out real);
5231 else if (key.Equals("Imaginary", StringComparison.OrdinalIgnoreCase))
5232 double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out imaginary);
5233 }
5234 }
5235 return new Complex(real, imaginary);
5236 }
5237
5238 internal static dynamic ParseComplex(string complexString)
5239 {
5240 string s = complexString.Trim();
5241
5242 string numPart = s.Substring(0, s.Length - 1); // remove 'i'
5243
5244 int signIndex = Math.Max(numPart.LastIndexOf('+'), numPart.LastIndexOf('-'));
5245 if (signIndex == -1) throw new FormatException("No sign found");
5246
5247 string realStr = numPart.Substring(0, signIndex); // may be empty or "-"
5248 string imagStr = numPart.Substring(signIndex); // "+4" or "-4"
5249
5250 double real = string.IsNullOrEmpty(realStr) ? 0 : double.Parse(realStr, CultureInfo.InvariantCulture);
5251 double imag = double.Parse(imagStr, CultureInfo.InvariantCulture);
5252
5253 return new Complex(real, imag);
5254 }
5255 // todo: handle dictionary, stack and hashset, add key parm
5256 public static bool AddToList(dynamic listQueueStackDictHashsetOrArray, dynamic value, int count)
5257 {
5258 bool retVal = true;
5259 value = EnsureCollectionType(listQueueStackDictHashsetOrArray, value);
5260
5261 if ((true == IsEnumerableObject(listQueueStackDictHashsetOrArray) && false == IsArray(listQueueStackDictHashsetOrArray)) || true == IsList(listQueueStackDictHashsetOrArray))
5262 {
5263 // It's an IEnumerable<OutputClass>
5264 try
5265 {
5266 MethodInfo addMethod = listQueueStackDictHashsetOrArray.GetType().GetMethod("Add",
5267 BindingFlags.Public | BindingFlags.Instance);
5268 MethodInfo enqueueMethod = listQueueStackDictHashsetOrArray.GetType().GetMethod("Enqueue",
5269 BindingFlags.Public | BindingFlags.Instance);
5270 MethodInfo pushMethod = listQueueStackDictHashsetOrArray.GetType().GetMethod("Push",
5271 BindingFlags.Public | BindingFlags.Instance);
5272
5273 if (null != addMethod)
5274 {
5275 if (true == ConverterHelper.IsDictionaryType(listQueueStackDictHashsetOrArray.GetType()))
5276 addMethod.Invoke(listQueueStackDictHashsetOrArray, new object[] { count, value });
5277 else
5278 addMethod.Invoke(listQueueStackDictHashsetOrArray, new object[] { value });
5279 }
5280 else if (null != enqueueMethod)
5281 enqueueMethod.Invoke(listQueueStackDictHashsetOrArray, new object[] { value });
5282 else if (null != pushMethod)
5283 pushMethod.Invoke(listQueueStackDictHashsetOrArray, new object[] { value });
5284 }
5285 catch (Exception ex)
5286 {
5287 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Can't Add [{value}] to [{listQueueStackDictHashsetOrArray}]. May require a Convert.ChangeType", listQueueStackDictHashsetOrArray, GPALObjectType.Other, ex);
5288 retVal = false;
5289 }
5290
5291 }
5292 else if (true == IsArray(listQueueStackDictHashsetOrArray))
5293 listQueueStackDictHashsetOrArray[count] = EnsureCollectionType(listQueueStackDictHashsetOrArray, value);
5294 // json/yaml will collapse single item arrays/lists to a scalar which will cause a collection mismatch, thus ensurecollectiontypes
5295
5296
5297 return retVal;
5298 }
5306 private static object EnsureCollectionType(object container, object value)
5307 {
5308 if (container == null || value == null) return value;
5309 Type containerType = container.GetType();
5310
5311 // Inlined element type detection (e.g., List<Complex> -> Complex)
5312 Type targetElementType = containerType.IsArray ? containerType.GetElementType() : (containerType.IsGenericType ? containerType.GetGenericArguments()[0] : null);
5313
5314 // If the value already matches the container (e.g., List matches List), return as is
5315 // If targetElementType is null, we can't determine how to wrap
5316 if (targetElementType == null || targetElementType.IsAssignableFrom(value.GetType()))
5317 return value;
5318
5319 // Ensure the scalar matches the internal element type (converting if it's a string representation)
5320 object convertedItem = ConvertValue(value, targetElementType, container);
5321
5322 // Now wrap the (potentially converted) item into the collection
5323 return WrapInCollection(targetElementType, convertedItem);
5324 }
5325
5326 //private static object ConvertValue(object value, Type targetType)
5327 //{
5328 // if (value == null || targetType.IsAssignableFrom(value.GetType())) return value;
5329
5330 // try
5331 // {
5332 // // Handle standard types (int, double, bool, etc.)
5333 // if (typeof(IConvertible).IsAssignableFrom(targetType))
5334 // return Convert.ChangeType(value, targetType);
5335
5336 // // Handle complex types from strings (Guid, IPAddress, etc.)
5337 // var converter = System.ComponentModel.TypeDescriptor.GetConverter(targetType);
5338 // if (converter != null && converter.CanConvertFrom(value.GetType()))
5339 // return converter.ConvertFrom(value);
5340 // }
5341 // catch { /* Fallback to original value if conversion fails */ }
5342
5343 // return value;
5344 //}
5345
5346 private static object WrapInCollection(Type typeToCreate, dynamic item)
5347 {
5348 // If the expected type is an array, create a 1-length array
5349 if (typeToCreate.IsArray)
5350 {
5351 Array newArray = Array.CreateInstance(typeToCreate.GetElementType(), 1);
5352 newArray.SetValue(item, 0);
5353 return newArray;
5354 }
5355
5356 if (typeToCreate == item.GetType())
5357 return item;
5358
5359 // If the expected type is a generic collection (List, Queue, Stack)
5360 object collection = Activator.CreateInstance(typeToCreate);
5361
5362 // Find the correct method to add the item based on collection type
5363 MethodInfo addMethod = typeToCreate.GetMethod("Add")
5364 ?? typeToCreate.GetMethod("Push")
5365 ?? typeToCreate.GetMethod("Enqueue");
5366
5367 addMethod?.Invoke(collection, new[] { item });
5368 return collection;
5369 }
5370
5371 internal static string UppercaseFirst(string input)
5372 {
5373 if (string.IsNullOrEmpty(input))
5374 return input;
5375
5376 return char.ToUpper(input[0]) + input.Substring(1);
5377 }
5378 // written by grok
5379 static bool CheckKeysPrefix(Dictionary<dynamic, dynamic> dict)
5380 {
5381 if (!dict.Any()) return true; // Empty case
5382
5383 string commonPrefix = null;
5384
5385 foreach (var entry in dict.Values)
5386 {
5387 // Each entry.Value should be a dictionary-like object with one property
5388 // We need to get its keys (property names)
5389
5390 // Using dynamic, we can enumerate keys via .GetEnumerator() or cast
5391 IEnumerable<string> innerKeys;
5392
5393 try
5394 {
5395 // Try to get the keys from the inner dynamic object
5396 // This works if it's ExpandoObject or similar
5397 var innerDict = (IDictionary<string, object>)entry;
5398 innerKeys = innerDict.Keys;
5399 }
5400 catch
5401 {
5402 // Fallback: reflect or handle other dynamic types
5403 return false;
5404 }
5405
5406 if (!innerKeys.Any())
5407 return false; // Empty object?
5408
5409 string currentKey = innerKeys.First(); // Assuming one key per object
5410
5411 string prefix = GetPrefixUpToNumber(currentKey);
5412
5413 if (string.IsNullOrEmpty(prefix))
5414 return false;
5415
5416 if (commonPrefix == null)
5417 {
5418 commonPrefix = prefix;
5419 }
5420 else if (currentKey != prefix && !currentKey.StartsWith(commonPrefix))
5421 {
5422 return false;
5423 }
5424 }
5425
5426 return true;
5427 }
5428
5429 static string GetPrefixUpToNumber(string key)
5430 {
5431 if (string.IsNullOrEmpty(key)) return string.Empty;
5432
5433 int i = 0;
5434 while (i < key.Length && !char.IsDigit(key[i]))
5435 i++;
5436
5437 return i > 0 ? key.Substring(0, i) : string.Empty;
5438 }
5439 public static Type GetConcreteType(Type type)
5440 {
5441 if (type == null) throw new ArgumentNullException(nameof(type));
5442
5443 // If type is already a concrete class (not abstract, not an interface), return it
5444 if (!type.IsInterface && !type.IsAbstract)
5445 {
5446 return type;
5447 }
5448
5449 // If type is an interface or abstract, find a concrete implementation
5450 return type.Assembly.GetTypes()
5451 .FirstOrDefault(t => !t.IsInterface && !t.IsAbstract && type.IsAssignableFrom(t))
5452 ?? throw new InvalidOperationException($"No concrete implementation found for {type.Name}.");
5453 }
5454 private static object ConvertValue(dynamic value, Type targetType, dynamic parentDictionary = null, string propertyName = null)
5455 {
5456 string fromCtx = propertyName != null ? $" from [{propertyName}]" : string.Empty;
5457
5458 // Handle null / empty / "null"
5459 bool isNullInput = false;
5460
5461 if (targetType == typeof(object))
5462 return value; // passthrough
5463
5464 // === Tuple handling (ValueTuple<> and Tuple<>) ===
5465 if (IsTupleType(value?.GetType()) && IsTupleType(targetType))
5466 {
5467 Type targetTupleType = Nullable.GetUnderlyingType(targetType) ?? targetType;
5468
5469 // Extract elements from source tuple using ITuple (available in .NET 4.7+ / .NET Core)
5470 if (value is System.Runtime.CompilerServices.ITuple sourceTuple)
5471 {
5472 int arity = sourceTuple.Length;
5473 Type[] targetElementTypes = targetTupleType.GetGenericArguments();
5474
5475 // Arity must match (we don't support automatic padding/truncation)
5476 if (arity == targetElementTypes.Length)
5477 {
5478 var convertedItems = new object[arity];
5479 for (int i = 0; i < arity; i++)
5480 {
5481 object item = sourceTuple[i];
5482 convertedItems[i] = ConvertValue(item, targetElementTypes[i]);
5483 }
5484
5485 // Now create the target tuple via constructor
5486 // All ValueTuple<> and Tuple<> types have a constructor taking each element
5487 var ctor = targetTupleType.GetConstructor(targetElementTypes);
5488 if (ctor != null)
5489 {
5490 return ctor.Invoke(convertedItems);
5491 }
5492 }
5493 }
5494 }
5495
5496 try
5497 {
5498 bool isStruct = IsCustomStructType(value?.GetType());
5499
5500 Type ut2 = Nullable.GetUnderlyingType(targetType) ?? targetType;
5501 bool isCharTarget = ut2 == typeof(char);
5502 isNullInput = false == isStruct
5503 ? value == null || // NOTE: comparing some types to null throws an exception, latest one valuetuple
5504 (value is string str && !isCharTarget && string.IsNullOrWhiteSpace(str)) ||
5505 (value is string s2 && s2.Trim().Equals("null", StringComparison.OrdinalIgnoreCase))
5506 : false;
5507 }
5508 catch
5509 {
5510 isNullInput = true;
5511 }
5512
5513 if (isNullInput)
5514 {
5515 if (IsNullableType(targetType) || targetType.IsClass)
5516 return null;
5517
5518 GPAL.PublishSimpleEvent(GPALEventType.ERROR,
5519 $"Cannot convert [{DescribeForLog(value)}] to non-nullable [{ShortTypeName(targetType)}]{fromCtx}. Returning default.",
5520 null, GPALObjectType.Other);
5521 return Activator.CreateInstance(targetType);
5522 }
5523
5524 Type underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
5525
5526 // === Collection handling ===
5527 if (IsSupportedCollectionType(underlyingTargetType))
5528 {
5529 Type elementType = underlyingTargetType.IsArray
5530 ? underlyingTargetType.GetElementType()!
5531 : underlyingTargetType.GetGenericArguments()[0];
5532
5533 IEnumerable<object> sourceItems = (value is IEnumerable enumerable && !(value is string))
5534 ? enumerable.Cast<object>()
5535 : new[] { value };
5536
5537 var convertedItems = new List<object>();
5538 int index = 0;
5539 foreach (var item in sourceItems)
5540 {
5541 object converted = ConvertValue(item, elementType);
5542 convertedItems.Add(converted);
5543 index++;
5544 }
5545
5546 return CreateCollection(convertedItems, underlyingTargetType);
5547 }
5548
5549 // === Scalar conversion ===
5550
5551 // Special float/double: NaN, Infinity, -Infinity – accept .nan, .inf, etc.
5552 if (value is string floatStr)
5553 {
5554 string trimmed = floatStr.Trim().ToLowerInvariant();
5555 if (underlyingTargetType == typeof(float) || underlyingTargetType == typeof(double))
5556 {
5557 if (trimmed == "nan" || trimmed == ".nan")
5558 return underlyingTargetType == typeof(float) ? (object)float.NaN : double.NaN;
5559 if (trimmed == "infinity" || trimmed == ".inf" || trimmed == "+infinity" || trimmed == "+.inf")
5560 return underlyingTargetType == typeof(float) ? (object)float.PositiveInfinity : double.PositiveInfinity;
5561 if (trimmed == "-infinity" || trimmed == "-.inf")
5562 return underlyingTargetType == typeof(float) ? (object)float.NegativeInfinity : double.NegativeInfinity;
5563 }
5564 }
5565
5566 // === Enum handling (any enum, including flags and nullable) ===
5567 if (IsEnumType(underlyingTargetType))
5568 {
5569 string strValue = value.ToString().Trim();
5570
5571 // Dynamic generic TryParse<TEnum>(string, bool ignoreCase, out TEnum result)
5572 var tryParseMethod = typeof(Enum)
5573 .GetMethods(BindingFlags.Public | BindingFlags.Static)
5574 .FirstOrDefault(m =>
5575 m.Name == nameof(Enum.TryParse)
5576 && m.IsGenericMethodDefinition
5577 && m.GetGenericArguments().Length == 1
5578 && m.GetParameters().Length == 3
5579 && m.GetParameters()[0].ParameterType == typeof(string)
5580 && m.GetParameters()[1].ParameterType == typeof(bool)
5581 && m.GetParameters()[2].IsOut);
5582
5583 if (tryParseMethod != null)
5584 {
5585 var genericMethod = tryParseMethod.MakeGenericMethod(underlyingTargetType);
5586 object[] parameters = new object[] { strValue, true, null }; // ignoreCase = true
5587 bool success = (bool)genericMethod.Invoke(null, parameters);
5588 if (success)
5589 {
5590 return parameters[2]; // the parsed enum value
5591 }
5592 }
5593
5594 // Fallback: try parsing as numeric value (common in serialized data)
5595 if (long.TryParse(strValue, out long numericValue))
5596 {
5597 try
5598 {
5599 return Enum.ToObject(underlyingTargetType, numericValue);
5600 }
5601 catch
5602 {
5603 // Invalid numeric value for this enum (out of range)
5604 }
5605 }
5606
5607 // If all else fails — return default (0) with warning
5608 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
5609 $"Invalid enum value [{value}] for type [{ShortTypeName(underlyingTargetType)}]{fromCtx}. Returning default (0).",
5610 null, GPALObjectType.Other);
5611
5612 return Enum.ToObject(underlyingTargetType, 0);
5613 }
5614
5615 // === Custom parsing for non-ChangeType types ===
5616 if (value is string strValue2)
5617 {
5618 string trimmed = strValue2.Trim();
5619
5620 if (IsGuidType(underlyingTargetType))
5621 {
5622 if (Guid.TryParse(trimmed, out Guid guid))
5623 return guid;
5624 // Strip leading _ added by SanitizeXmlElementName for digit-starting dictionary keys
5625 if (trimmed.Length > 1 && trimmed[0] == '_' && Guid.TryParse(trimmed.Substring(1), out Guid strippedGuid))
5626 return strippedGuid;
5627 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Guid format: [{strValue2}]{fromCtx}. Returning Guid.Empty.", null, GPALObjectType.Other);
5628 return Guid.Empty;
5629 }
5630
5631 if (IsIpAddressType(underlyingTargetType))
5632 {
5633 if (IPAddress.TryParse(trimmed, out IPAddress ip))
5634 return ip;
5635 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid IPAddress format: [{strValue2}]{fromCtx}. Returning IPAddress.Any.", null, GPALObjectType.Other);
5636 return IPAddress.Any;
5637 }
5638
5639 if (IsUriType(underlyingTargetType))
5640 {
5641 if (Uri.TryCreate(trimmed, UriKind.RelativeOrAbsolute, out Uri uri))
5642 return uri;
5643 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Uri format: [{strValue2}]{fromCtx}. Returning null.", null, GPALObjectType.Other);
5644 return null;
5645 }
5646
5647 if (IsTimeSpanType(underlyingTargetType))
5648 {
5649 if (TimeSpan.TryParse(trimmed, CultureInfo.InvariantCulture, out TimeSpan ts))
5650 return ts;
5651 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid TimeSpan format: [{strValue2}]{fromCtx}. Returning TimeSpan.Zero.", null, GPALObjectType.Other);
5652 return TimeSpan.Zero;
5653 }
5654
5655 // RoundtripKind so a time written as utc comes back as utc. Convert.ChangeType below reads the
5656 // trailing Z and hands back the same instant in local time, which changes both the Kind and the
5657 // text a round trip writes out. anything this cannot parse falls through to the old path
5658 if (IsDateTimeType(underlyingTargetType)
5659 && DateTime.TryParse(trimmed, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime roundTripped))
5660 return roundTripped;
5661
5662 // RoundtripKind so the written offset is taken as it stands rather than shifted to local time,
5663 // which is the only reason to be using DateTimeOffset rather than DateTime
5664 if (IsDateTimeOffsetType(underlyingTargetType))
5665 {
5666 if (DateTimeOffset.TryParse(trimmed, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset dto))
5667 return dto;
5668 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid DateTimeOffset format: [{strValue2}]{fromCtx}. Returning DateTimeOffset.MinValue.", null, GPALObjectType.Other);
5669 return DateTimeOffset.MinValue;
5670 }
5671
5672 if (IsBigIntegerType(underlyingTargetType))
5673 {
5674 if (BigInteger.TryParse(trimmed, NumberStyles.Integer, CultureInfo.InvariantCulture, out BigInteger bi))
5675 return bi;
5676 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid BigInteger format: [{strValue2}]{fromCtx}. Returning 0.", null, GPALObjectType.Other);
5677 return BigInteger.Zero;
5678 }
5679
5680 if (IsComplexType(underlyingTargetType))
5681 {
5682 try
5683 {
5684 return ParseComplex(trimmed);
5685 }
5686 catch
5687 {
5688 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Complex format: [{strValue2}]{fromCtx}. Returning 0+0i.", null, GPALObjectType.Other);
5689 return new Complex(0, 0);
5690 }
5691 }
5692
5693 if (IsVersionType(underlyingTargetType))
5694 {
5695 if (System.Version.TryParse(trimmed, out System.Version ver))
5696 return ver;
5697 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid Version format: [{strValue2}]{fromCtx}. Returning 0.0.", null, GPALObjectType.Other);
5698 return new System.Version(0, 0);
5699 }
5700 }
5701
5702 // === Version is a special type
5703 if (true == IsVersionType(underlyingTargetType))
5704 {
5705 if (value.GetType() == typeof(System.Version))
5706 return value;
5707
5708 List<int> versionInfo = new List<int>();
5709 int count = 4;
5710
5711 foreach (KeyValuePair<object, object> item in value)
5712 {
5713 if (0 == count--)
5714 break;
5715 versionInfo.Add(Int32.Parse(item.Value.ToString()));
5716 }
5717
5718 return new System.Version(versionInfo[0], versionInfo[1], versionInfo[2], versionInfo[3]);
5719 }
5720
5721 if (typeof(WaitTime) == underlyingTargetType)
5722 {
5723 WaitTime wt;
5724 if (true == TryConvertToWaitTime(value, out wt))
5725 return wt;
5726 }
5727
5728 if (true == IsCustomStructType(underlyingTargetType))
5729 {
5730 try
5731 {
5732 if (value.GetType() == underlyingTargetType)
5733 return value;
5734 else
5735 return ConvertToClass(value, underlyingTargetType, parentDictionary, null);
5736 }
5737 catch (Exception ex)
5738 {
5739 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
5740 $"Failed to convert [{value}] [{ShortTypeName(value?.GetType())}] to [{ShortTypeName(underlyingTargetType)}]{fromCtx}. Returning default.",
5741 null, GPALObjectType.Other, ex);
5742 return Activator.CreateInstance(underlyingTargetType);
5743 }
5744 }
5745 // BigInteger is listed in IsSimpleType but doesn't implement IConvertible — Convert.ChangeType fails.
5746 // Handle it explicitly before the generic simple-type path.
5747 if (IsBigIntegerType(underlyingTargetType))
5748 {
5749 try { return AsBigInteger(value); }
5750 catch
5751 {
5752 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Invalid BigInteger value: [{value}]{fromCtx}. Returning 0.", null, GPALObjectType.Other);
5753 return BigInteger.Zero;
5754 }
5755 }
5756
5757 // === Simple built-in types (primitives, decimal, DateTime) ===
5758 if (IsSimpleType(underlyingTargetType))
5759 {
5760 // BigInteger doesn't implement IConvertible — Convert.ChangeType(BigInteger, T) always throws.
5761 // Route through its string representation, which Convert.ChangeType handles for all numeric types.
5762 if (value is BigInteger bigIntSrc)
5763 {
5764 try { return System.Convert.ChangeType(bigIntSrc.ToString(CultureInfo.InvariantCulture), underlyingTargetType, CultureInfo.InvariantCulture); }
5765 catch (Exception ex)
5766 {
5767 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Failed to convert BigInteger [{value}] to [{ShortTypeName(underlyingTargetType)}]{fromCtx}. Returning default.", null, GPALObjectType.Other, ex);
5768 return Activator.CreateInstance(underlyingTargetType);
5769 }
5770 }
5771 try
5772 {
5773 return System.Convert.ChangeType(value, underlyingTargetType, CultureInfo.InvariantCulture);
5774 }
5775 catch (Exception ex)
5776 {
5777 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
5778 $"Failed to convert [{value}] [{ShortTypeName(value?.GetType())}] to [{ShortTypeName(underlyingTargetType)}]{fromCtx}. Returning default.",
5779 null, GPALObjectType.Other, ex);
5780 return Activator.CreateInstance(underlyingTargetType);
5781 }
5782 }
5783
5784 // already the right type — no conversion needed
5785 if (targetType.IsAssignableFrom(value.GetType()))
5786 return value;
5787
5788 // Cycle sentinel — "Recursion to [TypeName]" strings are placeholders written during
5789 // serialization when a cycle is detected. They cannot be reconstructed; return null/default.
5790 if (value is string cvCycleStr && cvCycleStr.StartsWith("Recursion to [") && cvCycleStr.EndsWith("]") && targetType != typeof(string))
5791 return targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
5792
5793 // dict → POCO: delegate to the full reconstruction engine
5794 if (value is IDictionary dictSrc && IsClassType(targetType) && !IsDictionaryType(targetType))
5795 {
5796 try { return CreateObjectFromDictionary(dictSrc, targetType, dictSrc, null); }
5797 catch (Exception ex)
5798 {
5799 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to reconstruct [{ShortTypeName(targetType)}] from dict{fromCtx}", value, GPALObjectType.None, ex);
5800 }
5801 }
5802
5803 // List wrapping a POCO — peel layers until we reach a dict or give up.
5804 if (value is IList cvListVal && IsClassType(targetType) && !IsDictionaryType(targetType) && !IsSupportedCollectionType(targetType))
5805 {
5806 object cvInner = cvListVal;
5807 while (cvInner is IList cvInnerList && cvInnerList.Count == 1)
5808 cvInner = cvInnerList[0];
5809 if (cvInner is IDictionary cvInnerDict)
5810 {
5811 try { return CreateObjectFromDictionary(cvInnerDict, targetType, cvInnerDict, null); }
5812 catch (Exception ex)
5813 {
5814 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to reconstruct [{ShortTypeName(targetType)}] from unwrapped dict{fromCtx}", value, GPALObjectType.None, ex);
5815 }
5816 }
5817 }
5818
5819 // === Final fallback ===
5820 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
5821 $"Unable to convert [{DescribeForLog(value)}] to [{ShortTypeName(targetType)}]{fromCtx}",
5822 null, GPALObjectType.Other);
5823 return value;
5824 }
5825
5826 private static string ShortTypeName(Type t)
5827 {
5828 if (t == null) return "null";
5829 if (IsNullableType(t))
5830 return ShortTypeName(Nullable.GetUnderlyingType(t)) + "?";
5831 if (!t.IsGenericType) return t.Name;
5832 if (IsTupleType(t))
5833 return "(" + string.Join(", ", t.GetGenericArguments().Select(a => ShortTypeName(a))) + ")";
5834 string baseName = t.Name.Substring(0, t.Name.IndexOf('`'));
5835 string args = string.Join(", ", t.GetGenericArguments().Select(a => ShortTypeName(a)));
5836 return $"{baseName}<{args}>";
5837 }
5838
5839 private static string CompactValue(object v, int depth = 0)
5840 {
5841 if (v == null) return "null";
5842 if (v is string s) return s.Length <= 40 ? s : s.Substring(0, 40) + "...";
5843 if (depth >= 1)
5844 {
5845 if (v is IDictionary) return "{...}";
5846 if (v is IEnumerable) return "[...]";
5847 }
5848 if (v is IDictionary dict)
5849 {
5850 var pairs = new List<string>();
5851 foreach (dynamic kvp in dict) { pairs.Add($"{kvp.Key}={CompactValue(kvp.Value, depth + 1)}"); if (pairs.Count >= 3) break; }
5852 return "{" + string.Join(", ", pairs) + (dict.Count > 3 ? ", ..." : "") + "}";
5853 }
5854 if (v is IEnumerable seq)
5855 {
5856 var items = new List<string>();
5857 foreach (var item in seq) { items.Add(CompactValue(item, depth + 1)); if (items.Count >= 2) break; }
5858 return "[" + string.Join(", ", items) + (items.Count >= 2 ? ", ..." : "") + "]";
5859 }
5860 try
5861 {
5862 string str = v.ToString();
5863 return str == v.GetType().FullName ? ShortTypeName(v.GetType()) : str;
5864 }
5865 catch { return ShortTypeName(v.GetType()); }
5866 }
5867
5868 private static string DescribeForLog(object value, int maxLen = 80)
5869 {
5870 if (value == null) return "null";
5871 if (value is string s)
5872 return s.Length <= maxLen ? s : s.Substring(0, maxLen) + "...";
5873 if (value is IDictionary dict)
5874 {
5875 var pairs = new List<string>();
5876 foreach (dynamic kvp in dict) { pairs.Add($"{kvp.Key}={CompactValue(kvp.Value)}"); if (pairs.Count >= 3) break; }
5877 return "{" + string.Join(", ", pairs) + (dict.Count > 3 ? ", ..." : "") + "}";
5878 }
5879 if (value is IEnumerable seq)
5880 {
5881 var items = new List<string>();
5882 foreach (var item in seq) { items.Add(CompactValue(item)); if (items.Count >= 3) break; }
5883 return "[" + string.Join(", ", items) + (items.Count >= 3 ? ", ..." : "") + "]";
5884 }
5885 try
5886 {
5887 string str = value.ToString();
5888 return str == value.GetType().FullName ? ShortTypeName(value.GetType()) : (str.Length <= maxLen ? str : str.Substring(0, maxLen) + "...");
5889 }
5890 catch { return ShortTypeName(value.GetType()); }
5891 }
5892
5893 private static object CreateCollection(List<object> convertedItems, Type targetCollectionType)
5894 {
5895 Type underlying = targetCollectionType;
5896 if (targetCollectionType.IsGenericType &&
5897 targetCollectionType.GetGenericTypeDefinition() == typeof(Nullable<>))
5898 {
5899 underlying = Nullable.GetUnderlyingType(targetCollectionType)!;
5900 }
5901
5902 if (underlying.IsArray)
5903 {
5904 Type elementType = underlying.GetElementType()!;
5905 Array array = Array.CreateInstance(elementType, convertedItems.Count);
5906 for (int i = 0; i < convertedItems.Count; i++)
5907 {
5908 object converted = ConvertValue(convertedItems[i], elementType);
5909 if (converted == null || elementType.IsAssignableFrom(converted.GetType()))
5910 array.SetValue(converted, i);
5911 else
5912 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to convert [{DescribeForLog(converted)}] to [{elementType.Name}]", converted, GPALObjectType.None);
5913 }
5914 return array;
5915 }
5916
5917 // Handle IList types: List<T>, IList<T>, etc.
5918 if (underlying.IsGenericType)
5919 {
5920 Type[] genericArgs = underlying.GetGenericArguments();
5921 Type elementType = genericArgs[0];
5922
5923 Type listType = typeof(List<>).MakeGenericType(elementType);
5924 IList list = (IList)Activator.CreateInstance(listType)!;
5925
5926 foreach (var rawItem in convertedItems)
5927 {
5928 object convertedItem = ConvertValue(rawItem, elementType);
5929 if (convertedItem == null || elementType.IsAssignableFrom(convertedItem.GetType()))
5930 list.Add(convertedItem);
5931 else
5932 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Unable to convert [{DescribeForLog(convertedItem)}] to [{elementType.Name}]", convertedItem, GPALObjectType.None);
5933 }
5934
5935 // If the original target was exactly List<T>, return it
5936 if (underlying.IsGenericType &&
5937 underlying.GetGenericTypeDefinition() == typeof(List<>))
5938 {
5939 return list;
5940 }
5941
5942 // Otherwise, try to construct the exact target type (e.g. ObservableCollection<T>, etc.)
5943 // Fallback to List<T> if constructor fails
5944 try
5945 {
5946 var targetListType = targetCollectionType; // may still be underlying
5947 var ctor = targetListType.GetConstructor(new[] { typeof(IEnumerable<>).MakeGenericType(elementType) });
5948 if (ctor != null)
5949 return ctor.Invoke(new object[] { list });
5950
5951 // Another common pattern: constructor taking ICollection<T> or just List<T>
5952 ctor = targetListType.GetConstructor(new[] { listType });
5953 if (ctor != null)
5954 return ctor.Invoke(new object[] { list });
5955 }
5956 catch { /* ignore - fall through */ }
5957
5958 // Final fallback
5959 return list;
5960 }
5961
5962 // Fallback for non-generic collections (rare)
5963 IList nonGenericList = new ArrayList();
5964 foreach (var item in convertedItems)
5965 nonGenericList.Add(item);
5966 return nonGenericList;
5967 }
5968
5969 private static object ConvertDictionaryToStruct(
5970 IDictionary<string, object> dict,
5971 Type structType,
5972 HashSet<object> visited)
5973 {
5974 // Create default struct instance
5975 object instance = Activator.CreateInstance(structType);
5976
5977 foreach (var prop in structType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
5978 {
5979 if (!prop.CanWrite)
5980 continue;
5981
5982 if (dict.TryGetValue(prop.Name, out var rawValue))
5983 {
5984 object converted = ConvertValue(
5985 rawValue,
5986 prop.PropertyType,
5987 rawValue);
5988
5989 prop.SetValue(instance, converted);
5990 }
5991 }
5992
5993 return instance;
5994 }
5995
5996
5997 public static object NormalizeForCleanJson(object input)
5998 {
5999 // NOTE
6000 if (input is Dictionary<object, dynamic> dict)
6001 {
6002 // Check if this dict looks like it came from XML (has @ or #text keys)
6003 bool hasXmlMarkers = dict.Keys.Cast<object>().Any(k => k.ToString().StartsWith("@GPAL") || k.ToString() == "#text");
6004
6005 if (!hasXmlMarkers)
6006 {
6007 // A dictionary key may itself be a complex object rather than a string/simple type
6008 // (ConvertClassToDictionary keeps such keys as the raw object). JSON objects only support
6009 // string keys, and flattening a complex key into one string is a one-way trip - there's
6010 // no way to reliably parse it back into the original object, especially if a field value
6011 // happens to contain whatever delimiter was used. Represent the whole dictionary as an
6012 // array of {key, value} pairs instead, so the key stays a real, structured, round-trippable
6013 // JSON object. GPALKEY_PAIRS marks this shape for the reader (see ConvertJSONToDictionary).
6014 bool hasComplexKey = dict.Keys.Cast<object>().Any(k => k != null && false == IsSimpleType(k.GetType()));
6015 if (hasComplexKey)
6016 {
6017 var pairList = new List<object>();
6018 foreach (var kvp in dict)
6019 {
6020 pairList.Add(new Dictionary<string, object>
6021 {
6022 ["Key"] = NormalizeForCleanJson(ConvertClassToDictionary(kvp.Key)),
6023 ["Value"] = NormalizeForCleanJson(kvp.Value)
6024 });
6025 }
6026 return pairList;
6027 }
6028
6029 // Pure generic dict, all-simple keys — normalize children recursively, but keep as-is at this level
6030 var result = new Dictionary<string, object>();
6031 foreach (var kvp in dict)
6032 {
6033 string key = kvp.Key.ToString().Replace("@GPAL_", "");
6034 result[key] = NormalizeForCleanJson(kvp.Value);
6035 }
6036 return result;
6037 }
6038
6039 // XML-derived: clean it up
6040 var clean = new Dictionary<string, object>();
6041
6042 string textValue = null;
6043 if (dict.TryGetValue("#text", out var txt))
6044 {
6045 textValue = txt?.ToString();
6046 }
6047
6048 // First: collect attributes (remove @ prefix)
6049 foreach (var kvp in dict)
6050 {
6051 string key = kvp.Key.ToString();
6052
6053 if (key.StartsWith("@GPAL_"))
6054 {
6055 clean[key.Substring(6)] = kvp.Value;
6056 }
6057 }
6058
6059 // Then: child elements
6060 foreach (var kvp in dict)
6061 {
6062 string key = kvp.Key.ToString();
6063 if (key.StartsWith("@GPAL_") || key == "#text") continue;
6064
6065 object child = NormalizeForCleanJson(kvp.Value);
6066
6067 clean[key] = child;
6068 }
6069
6070 // If this node has text and attributes (like <color_swatch image="...">Red</color_swatch>)
6071 // -> keep as object with attributes + value/text field
6072 if (textValue != null && clean.Count > 0)
6073 {
6074 clean["value"] = textValue;
6075 return clean;
6076 }
6077
6078 // If only text and no attributes/children -> flatten to direct string/number
6079 if (textValue != null && clean.Count == 0)
6080 {
6081 if (double.TryParse(textValue, out double num))
6082 return num;
6083 return textValue;
6084 }
6085
6086 return clean;
6087 }
6088 else if (input is List<Dictionary<object, dynamic>> list)
6089 {
6090 return list.Select(NormalizeForCleanJson).ToList();
6091 }
6092 else if (input is List<object> objList)
6093 {
6094 return objList.Select(NormalizeForCleanJson).ToList();
6095 }
6096
6097 return input; // primitive, leave as-is
6098 }
6099
6104 public static object StripGpalKeys(object input)
6105 {
6106 // Phase 1: Strip keys and correctly group repeated siblings
6107 object phase1 = StripAndGroup(input);
6108
6109 // Phase 2: Recursively unwrap unnecessary single-item lists
6110 return UnwrapSingleItemLists(phase1);
6111 }
6112
6113 private static object StripAndGroup(object input)
6114 {
6115 if (input == null) return null;
6116
6117 if (input is IDictionary dict)
6118 {
6119 var grouped = new Dictionary<string, List<object>>(StringComparer.Ordinal);
6120
6121 foreach (DictionaryEntry entry in dict)
6122 {
6123 string originalKey = entry.Key?.ToString() ?? string.Empty;
6124 string cleanedKey = originalKey;
6125
6126 if (cleanedKey.StartsWith("@GPAL_"))
6127 cleanedKey = cleanedKey.Substring(6); // -> @description
6128 else if (cleanedKey.StartsWith("@"))
6129 cleanedKey = cleanedKey.Substring(1);
6130
6131 object value = StripAndGroup(entry.Value);
6132
6133 // ConvertXmlNodeToDictionary always wraps each child node in a List<Dict> (even when there is
6134 // only one child with that tag name). Unwrap those single-item dict-lists here so that sibling
6135 // elements group correctly: e.g. three <item> siblings produce [pt1,pt2,pt3] not
6136 // [[pt1],[pt2],[pt3]]. Only unwrap when the single item is a dict; scalar single-item lists
6137 // (e.g. ["X"]) must stay wrapped so they round-trip as single-element collections.
6138 if (value is List<object> singleWrap && singleWrap.Count == 1 && singleWrap[0] is IDictionary)
6139 value = singleWrap[0];
6140
6141 // If this is text content with no key (common for mixed content elements like color_swatch)
6142 if (string.IsNullOrEmpty(cleanedKey) && value is string str && !string.IsNullOrWhiteSpace(str))
6143 cleanedKey = "value";
6144 // Flatten pure text nodes
6145 else if (cleanedKey == "#text")
6146 cleanedKey = "value";
6147 else if (cleanedKey.StartsWith("GPALKEY"))
6148 cleanedKey = cleanedKey.Substring(7);
6149 // ConvertXmlNodeToDictionary tags each child "GPAL###_tagname" (a global, ever-increasing
6150 // index) purely so same-named siblings don't collide as dictionary keys before grouping
6151 // happens right here - the index has no meaning past this point and must never reach
6152 // output. "GPALKEY" is excluded since it's letters, not digits, right after "GPAL", so
6153 // this can't collide with that other internal marker scheme.
6154 else if (System.Text.RegularExpressions.Regex.IsMatch(cleanedKey, @"^GPAL\d+_"))
6155 cleanedKey = System.Text.RegularExpressions.Regex.Replace(cleanedKey, @"^GPAL\d+_", "");
6156
6157 if (!grouped.TryGetValue(cleanedKey, out var list))
6158 {
6159 list = new List<object>();
6160 grouped[cleanedKey] = list;
6161 }
6162 list.Add(value);
6163 }
6164
6165 var result = new Dictionary<string, object>(StringComparer.Ordinal);
6166 foreach (var kvp in grouped)
6167 {
6168 result[kvp.Key] = kvp.Value.Count == 1 ? kvp.Value[0] : (object)kvp.Value;
6169 }
6170 // If this is a leaf text node (only one entry and it's "value"), promote it to the parent
6171 if (result.Count == 1 && result.ContainsKey("value"))
6172 {
6173 return result["value"]; // flatten pure text nodes to scalar
6174 }
6175 return result;
6176 }
6177
6178 if (input is IEnumerable enumerable && !(input is string))
6179 {
6180 var list = new List<object>();
6181 foreach (var item in enumerable)
6182 {
6183 list.Add(StripAndGroup(item));
6184 }
6185 return list.Count == 0 ? null : list;
6186 }
6187
6188 return input;
6189 }
6190
6191 private static object UnwrapSingleItemLists(object input)
6192 {
6193 if (input == null) return null;
6194
6195 // If it's a list with exactly one item -> unwrap it only if scalar (XML text node)
6196 // OR if inner is itself a list (collapsed from an {item: [...]} XML wrapper).
6197 if (input is IList list && list.Count == 1)
6198 {
6199 object inner = UnwrapSingleItemLists(list[0]);
6200 if (inner == null || inner is string || inner is ValueType)
6201 return inner;
6202 // Rule 3: if the inner item resolved to a list (e.g. from {item:[...]} collapse),
6203 // return it directly so the outer single-item wrapper doesn't re-box it.
6204 if (inner is IList)
6205 return inner;
6206 return new List<object> { inner };
6207 }
6208
6209 // If it's a list with >1 items -> always recurse into every child so nested
6210 // single-item list wrappers (XML artifacts) are fully unwrapped at every depth.
6211 if (input is IList multiList && multiList.Count > 1)
6212 {
6213 var unwrapped = new List<object>();
6214 foreach (var item in multiList)
6215 {
6216 unwrapped.Add(UnwrapSingleItemLists(item));
6217 }
6218 return unwrapped;
6219 }
6220
6221 // If it's a dictionary -> process values, then collapse {item: [list]} wrappers
6222 if (input is IDictionary dict)
6223 {
6224 var result = new Dictionary<string, object>(StringComparer.Ordinal);
6225 foreach (DictionaryEntry entry in dict)
6226 {
6227 string key = entry.Key?.ToString() ?? "";
6228 object value = UnwrapSingleItemLists(entry.Value);
6229 result[key] = value;
6230 }
6231 // Collapse pure text-node singleton
6232 if (result.Count == 1 && result.ContainsKey("value"))
6233 return result["value"];
6234 // Rule 2: collapse XML {item: [...]} wrapper dicts — "item" is the generic
6235 // element tag name GPAL uses for array children, not a real property name.
6236 // Scalar value means a single-element array was serialized; wrap it back in a list.
6237 if (result.Count == 1 && result.ContainsKey("item"))
6238 {
6239 var itemVal = result["item"];
6240 return itemVal is IList ? itemVal : new List<object> { itemVal };
6241 }
6242 return result;
6243 }
6244
6245 // Scalar -> return as-is
6246 return input;
6247 }
6248 internal static string GetXmlHeaderFromFile(string inputFilename)
6249 {
6250 if (!File.Exists(inputFilename))
6251 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
6252
6253 var lines = File.ReadAllLines(inputFilename);
6254 var headerLines = new List<string>();
6255
6256 foreach (var line in lines)
6257 {
6258 string trimmed = line.Trim();
6259
6260 // Stop when we hit the first actual element (starts with < followed by letter)
6261 if (trimmed.StartsWith("<") && !trimmed.StartsWith("<?") && !trimmed.StartsWith("<!DOCTYPE") && !trimmed.StartsWith("<!--"))
6262 {
6263 break;
6264 }
6265
6266 // Capture declaration, PI, DOCTYPE, comments if you want
6267 if (trimmed.StartsWith("<?xml") ||
6268 trimmed.StartsWith("<?xml-stylesheet") ||
6269 trimmed.StartsWith("<!DOCTYPE") ||
6270 trimmed.StartsWith("<!--"))
6271 {
6272 headerLines.Add(line); // preserve original indentation/whitespace
6273 }
6274 }
6275
6276 // Fallback if nothing found
6277 if (headerLines.Count == 0)
6278 return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
6279
6280 return string.Join(Environment.NewLine, headerLines) + Environment.NewLine;
6281 }
6282
6283 public static bool TryConvertToWaitTime(object value, out WaitTime result)
6284 {
6285 result = WaitTime.Immediate;
6286
6287 if (value == null)
6288 {
6290 GPALEventType.WARNING,
6291 "WaitTime treating NULL as ZERO",
6292 value,
6293 GPALObjectType.Converter);
6294
6295 result = 0;
6296 return true;
6297 }
6298
6299 if (value is WaitTime wt)
6300 {
6301 result = wt;
6302 return true;
6303 }
6304
6305 if (value is int i)
6306 {
6307 result = i;
6308 return true;
6309 }
6310
6311 if (value is string s)
6312 {
6313 if (WaitTime.TryFromString(s, out result))
6314 return true;
6315 }
6316
6317 if (WaitTime.TryFromDictionary((IDictionary<object, object>)value, out result))
6318 {
6319 return true;
6320 }
6321
6323 GPALEventType.WARNING,
6324 $"WaitTime conversion failed: unsupported type [{value.GetType().FullName}]",
6325 value,
6326 GPALObjectType.Converter);
6327
6328 return false;
6329 }
6330 internal static List<string> SplitRespectingQuotes(string line, char delimiter)
6331 {
6332 var tokens = new List<string>();
6333 var sb = new StringBuilder();
6334 bool inQuotes = false;
6335
6336 for (int i = 0; i < line.Length; i++)
6337 {
6338 char c = line[i];
6339
6340 if (c == '"')
6341 {
6342 // Handle escaped quotes ("")
6343 if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
6344 {
6345 sb.Append('"');
6346 i++; // skip escaped quote
6347 }
6348 else
6349 {
6350 inQuotes = !inQuotes;
6351 }
6352 }
6353 else if (c == delimiter && !inQuotes)
6354 {
6355 tokens.Add(sb.ToString().Trim());
6356 sb.Clear();
6357 }
6358 else if (c == delimiter && inQuotes)
6359 {
6360 // An embedded delimiter is only safe inside its original quotes. Once unquoted here,
6361 // a literal delimiter would re-split this token on a later re-parse/output pass -
6362 // replace it with a space instead of carrying it through.
6363 sb.Append(' ');
6364 }
6365 else
6366 {
6367 sb.Append(c);
6368 }
6369 }
6370
6371 tokens.Add(sb.ToString().Trim());
6372 return tokens;
6373 }
6374 internal static string ExtractRawXmlFromBrowserSource(string fullPageSource)
6375 {
6376 if (string.IsNullOrWhiteSpace(fullPageSource))
6377 return string.Empty;
6378
6379 // Look for the <div id="webkit-xml-viewer-source-xml"> ... </div>
6380 int startIndex = fullPageSource.IndexOf("<div id=\"webkit-xml-viewer-source-xml\">");
6381 if (startIndex == -1)
6382 {
6383 // Not wrapped — maybe it's already raw XML
6384 if (fullPageSource.TrimStart().StartsWith("<?xml"))
6385 return fullPageSource;
6386
6388 GPALEventType.DEBUG,
6389 "No webkit-xml-viewer wrapper found and content doesn't start with <?xml. Returning empty.",
6390 fullPageSource,
6391 GPALObjectType.Other);
6392 return string.Empty;
6393 }
6394
6395 startIndex += "<div id=\"webkit-xml-viewer-source-xml\">".Length;
6396
6397 int endIndex = fullPageSource.IndexOf("</div>", startIndex);
6398 if (endIndex == -1) endIndex = fullPageSource.Length;
6399
6400 string innerHtml = fullPageSource.Substring(startIndex, endIndex - startIndex).Trim();
6401
6402 // The inner content is usually the raw XML as text nodes
6403 // But in your sample, it's already the <sitemapindex>...</sitemapindex>
6404 // Sometimes it's escaped or has extra spans — strip if needed
6405
6406 // Quick clean-up if there are any HTML artifacts (rare for sitemaps)
6407 innerHtml = innerHtml.Replace("<![CDATA[", "").Replace("]]>", ""); // if CDATA wrapped
6408
6409 return innerHtml;
6410 }
6411 #endregion <Helpers>
6412 }
6413
6416 public static class ObjectFactory
6417 {
6423 public static T CreateInstance<T>() where T : class
6424 {
6425 Type type = typeof(T);
6426 var constructor = type.GetConstructor(
6427 BindingFlags.Instance | BindingFlags.NonPublic,
6428 null,
6429 Type.EmptyTypes,
6430 null
6431 );
6432
6433 if (constructor != null)
6434 {
6435 return (T)constructor.Invoke(new object[0]);
6436 }
6437 else
6438 {
6439 throw new InvalidOperationException($"No suitable constructor found for type {type.Name}.");
6440 }
6441 }
6442 }
6443}
static int CountGraphNodes(object root, int limit, out bool cyclic)
Walks an object graph far enough to answer two questions about it: is it big, and does it hold a cycl...
static object StripGpalKeys(object input)
Strips GPAL##_ prefixes and correctly handles repeated XML elements by turning sibling elements with ...
static List< Dictionary< object, dynamic > > ConvertHtmlToDictionary(dynamic inputDataOrFilename, bool isChildren=false)
static int CountGraphNodes(object root, int limit)
Counts nodes in an object graph, stopping at limit .
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static IAllowGridActions< string > Grid
New GPALGrid<string></string> (rows/columns).
Definition GPAL.cs:532
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568