using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; namespace Tango.Telemetry { internal static class DateTimeUtcFixer { /// /// Recursively scans the object graph starting at and /// sets Kind=Utc on any DateTime/DateTime? properties or collection elements that are Kind=Unspecified. /// Uses DateTime.SpecifyKind(value, DateTimeKind.Utc) (no clock adjustment). /// Notes: /// - Only properties are updated (not fields). /// - Collections: updates elements for IList/arrays and dictionary values; traverses other IEnumerable to reach nested objects. /// - Read-only properties (no setter) are skipped. /// - Root objects that are a DateTime value cannot be changed in place (struct); wrap them in a container if needed. /// public static void EnsureDateTimeUTC(object obj) { var visited = new HashSet(ReferenceEqualityComparer.Instance); EnsureUtcInternal(obj, visited); } private static void EnsureUtcInternal(object obj, HashSet visited) { if (obj == null) return; var type = obj.GetType(); // Avoid revisiting reference objects (handle cycles, EF proxies, etc.) if (!type.IsValueType) // only track reference types { if (!visited.Add(obj)) return; } // Handle dictionaries: update values, recurse into objects if (obj is IDictionary dict) { var keys = new List(); foreach (var k in dict.Keys) keys.Add(k); foreach (var key in keys) { var value = dict[key]; if (TrySpecifyUtcOnBoxedDateTime(ref value)) { dict[key] = value; } else { EnsureUtcInternal(value, visited); } } return; } // Handle lists/arrays: update elements, recurse into objects if (obj is IList list) { for (int i = 0; i < list.Count; i++) { var element = list[i]; if (TrySpecifyUtcOnBoxedDateTime(ref element)) { list[i] = element; } else { EnsureUtcInternal(element, visited); } } return; } // Traverse other enumerables just to reach nested objects (cannot replace DateTime elements) if (obj is IEnumerable enumerable && !(obj is string)) { foreach (var element in enumerable) { EnsureUtcInternal(element, visited); } } // Inspect properties const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; foreach (var prop in type.GetProperties(flags)) { // indexers not supported if (prop.GetIndexParameters().Length != 0) continue; // must be readable if (!prop.CanRead) continue; var pType = prop.PropertyType; // DateTime if (pType == typeof(DateTime)) { var val = (DateTime)prop.GetValue(obj); if (val.Kind == DateTimeKind.Unspecified) { var newVal = DateTime.SpecifyKind(val, DateTimeKind.Utc); TrySetProperty(obj, prop, newVal); } continue; } // Nullable var underlying = Nullable.GetUnderlyingType(pType); if (underlying == typeof(DateTime)) { var val = (DateTime?)prop.GetValue(obj); if (val.HasValue && val.Value.Kind == DateTimeKind.Unspecified) { var newVal = (DateTime?)DateTime.SpecifyKind(val.Value, DateTimeKind.Utc); TrySetProperty(obj, prop, newVal); } continue; } // Skip obvious simple types if (IsSimple(pType)) continue; // Recurse into complex objects var child = prop.GetValue(obj); EnsureUtcInternal(child, visited); } } private static bool TrySpecifyUtcOnBoxedDateTime(ref object obj) { if (obj == null) return false; var t = obj.GetType(); // Exact DateTime if (t == typeof(DateTime)) { var dt = (DateTime)obj; if (dt.Kind == DateTimeKind.Unspecified) { obj = DateTime.SpecifyKind(dt, DateTimeKind.Utc); } return true; } // Nullable boxed var underlying = Nullable.GetUnderlyingType(t); if (underlying == typeof(DateTime)) { // boxed Nullable can be unboxed to DateTime? safely var ndt = (DateTime?)obj; if (ndt.HasValue && ndt.Value.Kind == DateTimeKind.Unspecified) { obj = (DateTime?)DateTime.SpecifyKind(ndt.Value, DateTimeKind.Utc); } return true; } return false; } private static bool TrySetProperty(object target, PropertyInfo prop, object value) { try { // Prefer invoking the setter (even if non-public) var setter = prop.GetSetMethod(nonPublic: true); if (setter == null) return false; setter.Invoke(target, new[] { value }); return true; } catch { // Fall back to PropertyInfo.SetValue (may still work in some runtimes) try { prop.SetValue(target, value); return true; } catch { return false; } } } private static bool IsSimple(Type t) { if (t.IsPrimitive || t.IsEnum) return true; if (t == typeof(string) || t == typeof(decimal) || t == typeof(Guid) || t == typeof(Uri) || t == typeof(TimeSpan) || t == typeof(DateTimeOffset)) return true; // Value types other than DateTime/Nullable are considered simple return t.IsValueType; } /// Reference equality comparer for the visited set. private sealed class ReferenceEqualityComparer : IEqualityComparer { public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer(); public new bool Equals(object x, object y) => ReferenceEquals(x, y); public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj); } } }