using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text.Json; using System.Text.Json.Serialization; namespace MaksIT.Core.Extensions; public static class ObjectExtensions { /// /// Converts object to json string /// /// /// /// public static string ToJson(this T? obj) => obj.ToJson(null); /// /// Converts object to json string /// /// /// /// /// public static string ToJson(this T? obj, List? converters) { if (obj == null) return "{}"; var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; converters?.ForEach(x => options.Converters.Add(x)); return JsonSerializer.Serialize(obj, options); } /// /// Creates a deep clone of the object, preserving reference identity and supporting cycles. /// public static T DeepClone(this T source) { return (T)DeepCloneInternal(source, new Dictionary(ReferenceEqualityComparer.Instance)); } /// /// Deeply compares two objects for structural equality (fields, including private ones). /// public static bool DeepEqual(this T a, T b) { return DeepEqualInternal(a, b, new HashSet<(object, object)>(ReferencePairComparer.Instance)); } /// /// Copies all fields from the snapshot into the current target object (useful with tracked entities). /// public static void RevertFrom(this T target, T snapshot) { if (ReferenceEquals(target, snapshot) || target == null || snapshot == null) return; var visited = new Dictionary(ReferenceEqualityComparer.Instance); CopyAllFields(snapshot!, target!, snapshot!.GetType(), visited); } #region Internal Cloner private static object DeepCloneInternal(object source, Dictionary visited) { if (source == null) return null!; var type = source.GetType(); // Fast-path for immutable/primitive-ish types if (IsImmutable(type)) return source; // Already cloned? if (!type.IsValueType && visited.TryGetValue(source, out var existing)) return existing; // Arrays if (type.IsArray) return CloneArray((Array)source, visited); // Value types (structs): shallow copy via boxing + clone ref-type fields if (type.IsValueType) return CloneStruct(source, type, visited); // Reference type: allocate uninitialized object, then copy fields var clone = FormatterServices.GetUninitializedObject(type); visited[source] = clone; CopyAllFields(source, clone, type, visited); return clone; } private static bool IsImmutable(Type t) { if (t.IsPrimitive || t.IsEnum) return true; // Common immutable BCL types if (t == typeof(string) || t == typeof(decimal) || t == typeof(DateTime) || t == typeof(DateTimeOffset) || t == typeof(TimeSpan) || t == typeof(Guid) || t == typeof(Uri)) return true; // Nullable of immutable underlying if (Nullable.GetUnderlyingType(t) is Type nt) return IsImmutable(nt); return false; } private static Array CloneArray(Array source, Dictionary visited) { var elemType = source.GetType().GetElementType()!; var rank = source.Rank; var lengths = new int[rank]; var lowers = new int[rank]; for (int d = 0; d < rank; d++) { lengths[d] = source.GetLength(d); lowers[d] = source.GetLowerBound(d); } var clone = Array.CreateInstance(elemType, lengths, lowers); visited[source] = clone; var indices = new int[rank]; CopyArrayRecursive(source, clone, 0, indices, lowers, lengths, visited); return clone; } private static void CopyArrayRecursive( Array source, Array target, int dim, int[] indices, int[] lowers, int[] lengths, Dictionary visited) { if (dim == source.Rank) { var value = source.GetValue(indices); var cloned = DeepCloneInternal(value!, visited); target.SetValue(cloned, indices); return; } int start = lowers[dim]; int end = lowers[dim] + lengths[dim]; for (int i = start; i < end; i++) { indices[dim] = i; CopyArrayRecursive(source, target, dim + 1, indices, lowers, lengths, visited); } } private static object CloneStruct(object source, Type type, Dictionary visited) { // Boxed copy is already a shallow copy of the struct object boxed = source; CopyAllFields(boxed, boxed, type, visited, skipVisitedRegistration: true); return boxed; } private static void CopyAllFields(object source, object target, Type type, Dictionary visited, bool skipVisitedRegistration = false) { for (Type t = type; t != null; t = t.BaseType!) { var fields = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); foreach (var f in fields) { var value = f.GetValue(source); var cloned = DeepCloneInternal(value!, visited); f.SetValue(target, cloned); } } } #endregion #region Internal Deep-Equal private static bool DeepEqualInternal(object a, object b, HashSet<(object, object)> visited) { if (ReferenceEquals(a, b)) return true; if (a == null || b == null) return false; var type = a.GetType(); if (type != b.GetType()) return false; // Fast path for immutables if (IsImmutable(type)) return a.Equals(b); // Prevent infinite recursion on cycles var pair = (a, b); if (visited.Contains(pair)) return true; visited.Add(pair); // Arrays if (type.IsArray) return ArraysEqual((Array)a, (Array)b, visited); // Value or reference types: compare fields recursively return FieldsEqual(a, b, type, visited); } private static bool ArraysEqual(Array a, Array b, HashSet<(object, object)> visited) { if (a.Rank != b.Rank) return false; for (int d = 0; d < a.Rank; d++) { if (a.GetLength(d) != b.GetLength(d) || a.GetLowerBound(d) != b.GetLowerBound(d)) return false; } var indices = new int[a.Rank]; return CompareArrayRecursive(a, b, 0, indices, visited); } private static bool CompareArrayRecursive(Array a, Array b, int dim, int[] indices, HashSet<(object, object)> visited) { if (dim == a.Rank) { var va = a.GetValue(indices); var vb = b.GetValue(indices); return DeepEqualInternal(va!, vb!, visited); } int start = a.GetLowerBound(dim); int end = start + a.GetLength(dim); for (int i = start; i < end; i++) { indices[dim] = i; if (!CompareArrayRecursive(a, b, dim + 1, indices, visited)) return false; } return true; } private static bool FieldsEqual(object a, object b, Type type, HashSet<(object, object)> visited) { for (Type t = type; t != null; t = t.BaseType!) { var fields = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly); foreach (var f in fields) { var va = f.GetValue(a); var vb = f.GetValue(b); if (!DeepEqualInternal(va!, vb!, visited)) return false; } } return true; } #endregion #region Helpers 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); } private sealed class ReferencePairComparer : IEqualityComparer<(object, object)> { public static readonly ReferencePairComparer Instance = new ReferencePairComparer(); public bool Equals((object, object) x, (object, object) y) => ReferenceEquals(x.Item1, y.Item1) && ReferenceEquals(x.Item2, y.Item2); public int GetHashCode((object, object) obj) { unchecked { return (RuntimeHelpers.GetHashCode(obj.Item1) * 397) ^ RuntimeHelpers.GetHashCode(obj.Item2); } } } #endregion }