private static IEnumerable<ObjectVariance> CheckNullObjectsVariance(object object1, object object2, ObjectVariance parentVariance) { var propertyInfo1 = object1 as PropertyInfo; var propertyInfo2 = object2 as PropertyInfo; // One of the objects is null if (ReferenceEquals(object1, null) || (propertyInfo1 != null && ReferenceEquals(propertyInfo1.GetValue(parentVariance.PropertyValue1), null))) { // object2 is a PropertyInfo if (propertyInfo2 != null) { object property2Value = propertyInfo2.GetValue(parentVariance.PropertyValue2); yield return new ObjectVariance(propertyInfo2.Name, null, property2Value, parentVariance); yield break; } // object2 is a primitive or string, or DateTime yield return new ObjectVariance(null, null, object2, parentVariance); yield break; } if (!ReferenceEquals(object2, null) && (propertyInfo2 == null || !ReferenceEquals(propertyInfo2.GetValue(parentVariance.PropertyValue2), null))) { yield break; } // object1 is a PropertyInfo if (propertyInfo1 != null) { object property1Value = propertyInfo1.GetValue(parentVariance.PropertyValue1); yield return new ObjectVariance(propertyInfo1.Name, property1Value, null, parentVariance); yield break; } // object1 is a primitive or string, or DateTime yield return new ObjectVariance(null, object1, null, parentVariance); }
private static IEnumerable<ObjectVariance> GetVariancesFromProperties(object object1, object object2, ObjectVariance parentVariance) { // ReferenceEquals instead of == because == may be overridden if (ReferenceEquals(object1, null)) { throw new ArgumentNullException(nameof(object1)); } // ReferenceEquals instead of == because == may be overridden if (ReferenceEquals(object2, null)) { throw new ArgumentNullException(nameof(object2)); } IEnumerable<PropertyInfo> object1PropertyInfos = ReflectionHelper.GetPropertyInfos(object1.GetType()); IEnumerable<PropertyInfo> object2PropertyInfos = ReflectionHelper.GetPropertyInfos(object2.GetType()); using (IEnumerator<PropertyInfo> propertyInfo1Enumerator = object1PropertyInfos.GetEnumerator(), propertyInfo2Enumerator = object2PropertyInfos.GetEnumerator()) { while (propertyInfo1Enumerator.MoveNext() && propertyInfo2Enumerator.MoveNext()) { // Found IgnoreVarianceAttribute if (ReflectionHelper.HasIgnoreVarianceAttribute(propertyInfo1Enumerator.Current, propertyInfo2Enumerator.Current)) { continue; } foreach (ObjectVariance objectVariance in GetObjectVariances(propertyInfo1Enumerator.Current, propertyInfo2Enumerator.Current, parentVariance)) { yield return objectVariance; } } } }
private static IEnumerable<ObjectVariance> GetObjectVariances(object object1, object object2, ObjectVariance parentVariance) { // Base case where object1 and object2 are PropertyInfo objects already string propertyName = null; var propertyInfo1 = object1 as PropertyInfo; var propertyInfo2 = object2 as PropertyInfo; if (propertyInfo1 != null && propertyInfo2 != null && propertyInfo1.Name == propertyInfo2.Name) { // Found IgnoreVarianceAttribute if (ReflectionHelper.HasIgnoreVarianceAttribute(propertyInfo1, propertyInfo2)) { yield break; } propertyName = propertyInfo1.Name; // ReSharper disable once PossibleNullReferenceException object1 = ((PropertyInfo)object1).GetValue(parentVariance.PropertyValue1); object2 = ((PropertyInfo)object2).GetValue(parentVariance.PropertyValue2); // Check if already traversed if (!ReflectionHelper.ShouldIgnoreVariance(object1, object2) && AreObjectsTraversed(object1, object2)) { yield break; } } // Both objects are null, required for inner property checking if (ReferenceEquals(object1, null) && ReferenceEquals(object2, null)) { yield break; } // Null checks, two scenarios // 1. It is a propertyInfo that can be further traversed // 2. It is a top level object object nullCheck1 = propertyInfo1 ?? object1; object nullCheck2 = propertyInfo2 ?? object2; foreach ( ObjectVariance objectVariance in CheckNullObjectsVariance(nullCheck1, nullCheck2, parentVariance)) { // ReSharper disable once PossibleNullReferenceException yield return objectVariance; yield break; } // ReSharper disable once PossibleNullReferenceException // Checked with ReferenceEquals(object1, null) already Type object1Type = object1.GetType(); Type object2Type = object2.GetType(); // Special case where the two objects are primitive type, if the two objects are not primitives, // there are two possibilities: // 1. object1 & object2 are properties to other types which can be further traversed // 2. object1 & object2 are top level objects if (ReflectionHelper.ShouldIgnoreVariance(object1, object2)) { if (object1.Equals(object2)) { yield break; } // parentVariance == null means primitives are being passed to compare // propertyName != null means deepest level reached within the object graph yield return propertyName != null || parentVariance == null ? new ObjectVariance(propertyName, object1, object2, parentVariance) : parentVariance; yield break; } // parentVariance == null menas primitives are being passed passed to compare // propertyName != null means deepest level reached within the object graph if (propertyName != null || parentVariance == null) { parentVariance = new ObjectVariance(propertyName, object1, object2, parentVariance); } // For both objects implementing IEnumerable<T> Type object1GenericArgument, object2GenericArgument; if (ReflectionHelper.TryGetIEnumerableGenericArgument(object1Type, out object1GenericArgument) && ReflectionHelper.TryGetIEnumerableGenericArgument(object2Type, out object2GenericArgument) && object1GenericArgument == object2GenericArgument) { // Found IgnoreVarianceAttribute if (ReflectionHelper.HasIgnoreVarianceAttribute(object1GenericArgument)) { yield break; } IEnumerable<PropertyInfo> propertyInfos; IEnumerable<ObjectVariance> result = ReflectionHelper.TryGetKeyAttriubte(object1GenericArgument, out propertyInfos) ? GetEnumerableVariancesByKey(object1 as IEnumerable, object2 as IEnumerable, propertyInfos, parentVariance) : GetEnumerableVariancesByPosition(object1 as IEnumerable, object2 as IEnumerable, parentVariance); foreach (ObjectVariance objectVariance in result) { yield return objectVariance; } // Required for more than one difference in inner IEnumerables yield break; } // Add to traversed HashSet lock (TraversedObjectLock) { TraversedObjects.Add(object1); TraversedObjects.Add(object2); } // Compare by property foreach ( ObjectVariance objectVariance in GetVariancesFromProperties(object1, object2, parentVariance)) { yield return objectVariance; } }
private static IEnumerable<ObjectVariance> GetEnumerableVariancesByPosition(IEnumerable object1, IEnumerable object2, ObjectVariance parentVariance, object key = null) { // Boxing here, but we cannot determine what generic type argument the caller will pass List<object> value1List = object1.Cast<object>().ToList(); List<object> value2List = object2.Cast<object>().ToList(); // If the count of IEnumerable is not equal, the two IEnumerable are definitely unequal if (value1List.Count != value2List.Count) { yield return new ObjectVariance( $"{(parentVariance != null ? parentVariance.PropertyName : "this")}.Count()", value1List.Count, value2List.Count, parentVariance); // Yield break here as there is no reason to compare since their count is different yield break; } // Try to compare by position for (int i = 0; i < value1List.Count; i++) { // Optional key object used in here since GetEnumerableVariancesByKey will eventually route to here // propertyName will be assigned "this[i]" when comparing by position or key.ToString() when comparing by key string propertyName = key?.ToString() ?? $"this[{i}]"; if (AreObjectsTraversed(value1List[i], value2List[i])) { yield break; } // As a test variance for each element in the IEnumerable var testParentVariance = new ObjectVariance(propertyName, value1List[i], value2List[i], parentVariance); foreach ( ObjectVariance objectVariance in GetObjectVariances(value1List[i], value2List[i], testParentVariance)) { yield return objectVariance; } } }
private static IEnumerable<ObjectVariance> GetEnumerableVariancesByKey(IEnumerable enumerable1, IEnumerable enumerable2, IEnumerable<PropertyInfo> propertyInfos, ObjectVariance parentVariance) { List<string> propertyInfosList = propertyInfos.Select(p => p.Name).ToList(); string propertyNames = String.Join(", ", propertyInfosList); // Try to group by properties having the KeyAttribute applied using Dynamic LINQ // IGroupedEnumerable with Key => Anonymous object with the key properties, // Count => Count of grouped objects // Value => The grouped object themselves // Compared => Boolean flag for determining whether we can skip comparisons or not IQueryable query1 = enumerable1.AsQueryable() .GroupBy($"new ({propertyNames})", "it") .Select("new (it.Key, it.Count() as Count, it as Value, false as Compared)"); IQueryable query2 = enumerable2.AsQueryable() .GroupBy($"new ({propertyNames})", "it") .Select("new (it.Key, it.Count() as Count, it as Value, false as Compared)"); // Make sure if query1 contains more items than query2, or vice versa, can be detected foreach ( ObjectVariance objectVariance in KeyPropertiesComparer.GetSetDifferenceVariances(query1, query2, parentVariance)) { yield return objectVariance; } foreach (dynamic group1 in query1) { foreach (dynamic group2 in query2) { if (group1.Compared || group2.Compared) { continue; } if (!Object.Equals(group1.Key, group2.Key)) { continue; } if (group1.Count > 1 || group2.Count > 1) { throw new InvalidOperationException("The IEnumerable contains objects with the same key"); } // Here we already guaranteed there is one value per group for each key foreach ( ObjectVariance objectVariance in GetEnumerableVariancesByPosition(group1.Value, group2.Value, parentVariance, group1.Key)) { yield return objectVariance; } group1.Compared = true; group2.Compared = true; } } }
public static IEnumerable<ObjectVariance> GetSetDifferenceVariances(IQueryable query1, IQueryable query2, ObjectVariance parentVariance) { IQueryable keyQuery1 = query1.Select("it.Key"); IQueryable keyQuery2 = query2.Select("it.Key"); IEnumerable<object> keyList1 = from dynamic q in keyQuery1 select q; IEnumerable<object> keyList2 = from dynamic q in keyQuery2 select q; List<object> extraKeysIn1 = keyList1.Except(keyList2).ToList(); List<object> extraKeysIn2 = keyList2.Except(keyList1).ToList(); // Extra Keys in query1 foreach (var extraKey in extraKeysIn1) { foreach (dynamic group1 in query1) { if (group1.Count > 1) { throw new InvalidOperationException("The IEnumerable contains objects with the same key"); } if (!Object.Equals(group1.Key, extraKey)) { continue; } foreach (var object1 in group1.Value) { string propertyName = parentVariance != null ? !String.IsNullOrEmpty(parentVariance.PropertyName) ? parentVariance.PropertyName : "IEnumerable 1" : "IEnumerable 1"; yield return new ObjectVariance(String.Format("Extra object in {0} with key {1}", propertyName, group1.Key.ToString()), object1, null, parentVariance); } } } // Extra Keys in query2 foreach (var extraKey in extraKeysIn2) { foreach (dynamic group2 in query2) { if (group2.Count > 1) { throw new InvalidOperationException("The IEnumerable contains objects with the same key"); } if (!Object.Equals(group2.Key, extraKey)) { continue; } foreach (var object2 in group2.Value) { string propertyName = parentVariance != null ? !String.IsNullOrEmpty(parentVariance.PropertyName) ? parentVariance.PropertyName : "IEnumerable 2" : "IEnumerable 2"; yield return new ObjectVariance(String.Format("Extra object in {0} with key {1}", propertyName, group2.Key.ToString()), null, object2, parentVariance); } } } }