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