diff options
Diffstat (limited to 'Software/Visual_Studio/Tango.Telemetry/Helpers/DateTimeUTCFixer.cs')
| -rw-r--r-- | Software/Visual_Studio/Tango.Telemetry/Helpers/DateTimeUTCFixer.cs | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/Software/Visual_Studio/Tango.Telemetry/Helpers/DateTimeUTCFixer.cs b/Software/Visual_Studio/Tango.Telemetry/Helpers/DateTimeUTCFixer.cs new file mode 100644 index 000000000..3ce0c700f --- /dev/null +++ b/Software/Visual_Studio/Tango.Telemetry/Helpers/DateTimeUTCFixer.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Tango.Telemetry.Helpers +{ + internal static class DateTimeUtcFixer + { + /// <summary> + /// Recursively scans the object graph starting at <paramref name="obj"/> 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. + /// </summary> + public static void EnsureDateTimeUTC(object obj) + { + var visited = new HashSet<object>(ReferenceEqualityComparer.Instance); + EnsureUtcInternal(obj, visited); + } + + private static void EnsureUtcInternal(object obj, HashSet<object> 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<object>(); + 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<DateTime> + 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<DateTime> boxed + var underlying = Nullable.GetUnderlyingType(t); + if (underlying == typeof(DateTime)) + { + // boxed Nullable<DateTime> 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<DateTime> are considered simple + return t.IsValueType; + } + + /// <summary>Reference equality comparer for the visited set.</summary> + private sealed class ReferenceEqualityComparer : IEqualityComparer<object> + { + 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); + } + } +} |
