/// <summary> /// Returns if the object/property should be included or excluded /// </summary> /// <param name="name">Property or field name</param> /// <param name="path">Full path to object</param> /// <param name="options">Comparison options</param> /// <param name="propertiesToExcludeOrInclude">A list of property names or full path names to include/exclude. Default is <seealso cref="ComparisonOptions.ExcludeList"/>. Specify <seealso cref="ComparisonOptions.ExcludeList"/> to exclude the specified properties from the Diff or <seealso cref="ComparisonOptions.IncludeList"/> to only Diff properties contained in the list.</param> /// <returns></returns> private FilterResult GetPropertyInclusionState(string name, string path, ComparisonOptions options, ICollection <string> propertiesToExcludeOrInclude, IEnumerable <CustomAttributeData> attributes, ICollection <object> attributeIgnoreList) { var isIncludeList = options.BitwiseHasFlag(ComparisonOptions.IncludeList); // include list overrides behavior even if ExcludeList is provided (because its enabled by default, just exists for documentation purposes) var isExcludeList = !isIncludeList; var excludeByNameOrPath = isExcludeList && (propertiesToExcludeOrInclude?.Contains(name) == true || propertiesToExcludeOrInclude?.Contains(path) == true); if (excludeByNameOrPath) { return(FilterResult.Exclude); // exclude the property (by being in the exclusion list) } if (isIncludeList) { var noInheritance = options.BitwiseHasFlag(ComparisonOptions.IncludeListNoInheritance); var includeByNameOrPath = (propertiesToExcludeOrInclude?.Contains(name) == true || propertiesToExcludeOrInclude?.Contains(path) == true); if (!includeByNameOrPath && !noInheritance) { // for inclusion lists, if the parent path is allowed then allow its children var pathParts = path.Split('.'); var parentPaths = pathParts.Skip(1).Take(pathParts.Length - 2).Select(x => $".{x}").ToList(); foreach (var parentPath in parentPaths) { includeByNameOrPath = propertiesToExcludeOrInclude.Contains(parentPath); if (includeByNameOrPath) { break; } } } if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(path) || includeByNameOrPath) { return(FilterResult.Include); // include the property } return(FilterResult.Exclude); // exclude the property (by not being in inclusion list) } #if FEATURE_CUSTOM_ATTRIBUTES if (attributes?.Any(x => !options.BitwiseHasFlag(ComparisonOptions.DisableIgnoreAttributes) && (attributeIgnoreList.Contains(x.AttributeType) || attributeIgnoreList.Contains(x.AttributeType.Name))) == true) #else if (attributes?.Any(x => !options.BitwiseHasFlag(ComparisonOptions.DisableIgnoreAttributes) && (attributeIgnoreList.Contains(x.Constructor.DeclaringType) || attributeIgnoreList.Contains(x.Constructor.DeclaringType.Name))) == true) #endif { return(FilterResult.Exclude); // exclude the property (by attribute) } return(FilterResult.Include); // include the property (default) }
/// <summary> /// Get the differences between two objects /// </summary> /// <param name="propertyName">The name of the property being compared</param> /// <param name="propertyType">The type of property being compared. The left property is assumed unless allowCompareDifferentObjects=true</param> /// <param name="typeConverter">An optional TypeConverter to treat the type as a different type</param> /// <param name="left">The left object to compare</param> /// <param name="right">The right object to compare</param> /// <param name="parent">The parent object</param> /// <param name="differences">A list of differences currently found in the tree</param> /// <param name="currentDepth">The current depth of the tree recursion</param> /// <param name="maxDepth">The maximum number of tree children to recurse</param> /// <param name="objectTree">A hash table containing the tree that has already been traversed, to prevent recursion loops</param> /// <param name="path">The current path</param> /// <param name="options">Specify the comparison options</param> /// <param name="propertiesToExcludeOrInclude">A list of property names or full path names to include/exclude. Default is <seealso cref="ComparisonOptions.ExcludeList"/>. Specify <seealso cref="ComparisonOptions.ExcludeList"/> to exclude the specified properties from the Diff or <seealso cref="ComparisonOptions.IncludeList"/> to only Diff properties contained in the list.</param> /// <param name="diffOptions">Specify custom diff options</param> /// <returns></returns> private List <Difference> GetDifferences(string propertyName, Type propertyType, TypeConverter typeConverter, IEnumerable <CustomAttributeData> attributes, object left, object right, object parent, List <Difference> differences, int currentDepth, int maxDepth, ObjectHashcodeMap objectTree, string path, ComparisonOptions options, ICollection <string> propertiesToExcludeOrInclude, DiffOptions diffOptions) { var propertyTypeSupport = propertyType.GetExtendedType(DefaultTypeSupportOptions); var isCollection = propertyType != typeof(string) && propertyType.GetInterface(nameof(IEnumerable)) != null; object leftValue = null; object rightValue = null; leftValue = left; if (options.BitwiseHasFlag(ComparisonOptions.AllowCompareDifferentObjects) && rightValue != null) { rightValue = GetValueForProperty(right, propertyName); } else { rightValue = right; } if (!isCollection || (isCollection && !options.BitwiseHasFlag(ComparisonOptions.TreatEmptyListAndNullTheSame))) { if (rightValue == null && leftValue != null || leftValue == null && rightValue != null) { if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference((leftValue ?? rightValue).GetType(), propertyName, path, leftValue, rightValue, typeConverter)); } return(differences); } } if (leftValue == null && rightValue == null) { return(differences); } var leftValueType = leftValue?.GetType() ?? propertyType; var rightValueType = rightValue?.GetType() ?? propertyType; if (isCollection && options.BitwiseHasFlag(ComparisonOptions.CompareCollections)) { var genericArguments = propertyType.GetGenericArguments(); var isArray = propertyTypeSupport.IsArray; var elementType = propertyTypeSupport.ElementType; // iterate the collection var aValueCollection = (leftValue as IEnumerable); var aValueCollectionCount = GetCountFromEnumerable(aValueCollection); var bValueCollection = (rightValue as IEnumerable); var bValueCollectionCount = GetCountFromEnumerable(bValueCollection); var bValueEnumerator = bValueCollection?.GetEnumerator(); if (options.BitwiseHasFlag(ComparisonOptions.TreatEmptyListAndNullTheSame) && aValueCollectionCount == 0 && bValueCollectionCount == 0) { // skip collection equality check, they both have no elements or are null return(differences); } if (aValueCollection != null) { var leftIndex = 0; var matchTracker = new MatchTracker(); // compare elements must be the same order foreach (var collectionItem in aValueCollection) { // iterate the left side leftValue = collectionItem; matchTracker.AddLeft(leftValue, leftIndex); var hasMatch = false; var hasValue = false; if (options.BitwiseHasFlag(ComparisonOptions.AllowCollectionsToBeOutOfOrder)) { bValueEnumerator?.Reset(); } var rightIndex = 0; do { // iterate the right side ObjectHashcodeMap childObjectTree = null; // we can't use the object tree here when allowing out of order collections because we will compare the same collection element multiple times. if (!options.BitwiseHasFlag(ComparisonOptions.AllowCollectionsToBeOutOfOrder)) { childObjectTree = objectTree; } hasValue = bValueEnumerator?.MoveNext() ?? false; if (!hasValue) { if (leftIndex == rightIndex) { // left has a value in collection, right does not. That's a difference if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference(leftValue?.GetType() ?? elementType, propertyName, path, leftIndex, leftValue, null, typeConverter)); } } break; } rightValue = bValueEnumerator?.Current; matchTracker.AddRight(rightValue, rightIndex); rightIndex++; if (leftValue == null && rightValue == null) { continue; } if (leftValue == null && rightValue != null) { continue; } // check array element for difference if (leftValue != null && !leftValue.GetType().IsValueType&& leftValue.GetType() != typeof(string)) { var itemDifferences = RecurseProperties(leftValue, rightValue, parent, new List <Difference>(), currentDepth, maxDepth, childObjectTree, path, options, propertiesToExcludeOrInclude, diffOptions); if (itemDifferences.Any() && options.BitwiseHasFlag(ComparisonOptions.AllowCollectionsToBeOutOfOrder)) { continue; } else if (itemDifferences.Any()) { differences.AddRange(itemDifferences); hasMatch = true; } else { hasMatch = true; } } else if (leftValue != null && leftValue.GetType().IsGenericType&& leftValue.GetType().GetGenericTypeDefinition() == typeof(KeyValuePair <,>)) { // compare keys and values of a KVP var leftKvpKey = GetValueForProperty(leftValue, "Key"); var leftKvpValue = GetValueForProperty(leftValue, "Value"); var rightKvpKey = GetValueForProperty(rightValue, "Key"); var rightKvpValue = GetValueForProperty(rightValue, "Value"); var leftKvpKeyType = leftKvpKey?.GetType() ?? genericArguments.First(); var leftKvpValueType = leftKvpValue?.GetType() ?? genericArguments.Skip(1).First(); // compare the key if (leftKvpKey != null && !leftKvpKeyType.IsValueType && leftKvpKeyType != typeof(string)) { var itemDifferences = RecurseProperties(leftKvpKey, rightKvpKey, leftValue, new List <Difference>(), currentDepth, maxDepth, childObjectTree, path, options, propertiesToExcludeOrInclude, diffOptions); if (itemDifferences.Any() && options.BitwiseHasFlag(ComparisonOptions.AllowCollectionsToBeOutOfOrder)) { continue; } else if (itemDifferences.Any()) { if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.AddRange(itemDifferences); } hasMatch = true; } else { hasMatch = true; } } else { if (!IsMatch(leftKvpKey, rightKvpKey)) { if (options.BitwiseHasFlag(ComparisonOptions.AllowCollectionsToBeOutOfOrder)) { continue; } if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference(leftKvpKeyType, propertyName, path, leftIndex, leftKvpKey, rightKvpKey, typeConverter)); } hasMatch = true; break; } else { hasMatch = true; } } // compare the value if (leftKvpValue != null && !leftKvpValueType.IsValueType && leftKvpValueType != typeof(string)) { var itemDifferences = RecurseProperties(leftKvpValue, rightKvpValue, leftValue, new List <Difference>(), currentDepth, maxDepth, childObjectTree, path, options, propertiesToExcludeOrInclude, diffOptions); if (itemDifferences.Any() && options.BitwiseHasFlag(ComparisonOptions.AllowCollectionsToBeOutOfOrder)) { continue; } else if (itemDifferences.Any()) { if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.AddRange(itemDifferences); } hasMatch = true; } else { hasMatch = true; } } else { if (!IsMatch(leftValue, rightValue)) { if (options.BitwiseHasFlag(ComparisonOptions.AllowCollectionsToBeOutOfOrder)) { continue; } if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference(leftKvpValueType, propertyName, path, leftIndex, leftKvpValue, rightKvpValue, typeConverter)); } hasMatch = true; break; } else { hasMatch = true; } } } else { if (!IsMatch(leftValue, rightValue)) { if (options.BitwiseHasFlag(ComparisonOptions.AllowCollectionsToBeOutOfOrder)) { continue; } if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference(leftValue?.GetType() ?? elementType, propertyName, path, leftIndex, leftValue, rightValue, typeConverter)); } hasMatch = true; break; } else { hasMatch = true; } } if (hasMatch) { matchTracker.MatchLeft(leftValue, leftIndex); matchTracker.MatchRight(rightValue, rightIndex - 1); } } while (hasValue && !hasMatch); leftIndex++; } // check which elements were not matched to anything var rightUnmatched = matchTracker.GetRightUnmatched(); foreach (var unmatchedElement in rightUnmatched) { // dont add a difference if we already detected it if (!differences.Where(x => x.ArrayIndex == unmatchedElement.ArrayIndex && x.RightValue == unmatchedElement.Object).Any()) { if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference(unmatchedElement.Object?.GetType() ?? elementType, propertyName, path, unmatchedElement.ArrayIndex, null, unmatchedElement.Object, typeConverter)); } } } var leftUnmatched = matchTracker.GetLeftUnmatched(); foreach (var unmatchedElement in leftUnmatched) { // dont add a difference if we already detected it if (!differences.Where(x => x.ArrayIndex == unmatchedElement.ArrayIndex && x.LeftValue == unmatchedElement.Object).Any()) { if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference(unmatchedElement.Object?.GetType() ?? elementType, propertyName, path, unmatchedElement.ArrayIndex, unmatchedElement.Object, null, typeConverter)); } } } if (bValueCollectionCount > leftIndex) { // right side has extra elements var rightSideExtraElements = bValueCollectionCount - leftIndex; if (bValueEnumerator != null) { for (var i = 0; i < rightSideExtraElements; i++) { var hasValue = bValueEnumerator?.MoveNext() ?? false; if (hasValue) { if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference(aValueCollection.GetType(), propertyName, path, leftIndex, null, bValueEnumerator.Current, typeConverter)); } leftIndex++; } } } } matchTracker.Dispose(); } } else if (!leftValueType.IsValueType && leftValueType != typeof(string)) { if (leftValueType != rightValueType && leftValueType.BaseType == rightValueType.BaseType) { if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference(propertyType, propertyName, path, leftValue, rightValue, typeConverter)); } } else { differences = RecurseProperties(leftValue, rightValue, leftValue, differences, currentDepth, maxDepth, objectTree, path, options, propertiesToExcludeOrInclude, diffOptions); } } else { if (!IsMatch(leftValue, rightValue)) { if (GetPropertyInclusionState(propertyName, path, options, propertiesToExcludeOrInclude, attributes, diffOptions.AttributeIgnoreList) == FilterResult.Include) { differences.Add(new Difference(propertyType, propertyName, path, leftValue, rightValue, typeConverter)); } } } return(differences); }
/// <summary> /// Get the differences between two objects /// </summary> /// <param name="propertyName">The name of the property being compared</param> /// <param name="propertyType">The type of property being compared. The left property is assumed unless allowCompareDifferentObjects=true</param> /// <param name="typeConverter">An optional TypeConverter to treat the type as a different type</param> /// <param name="left">The left object to compare</param> /// <param name="right">The right object to compare</param> /// <param name="parent">The parent object</param> /// <param name="differences">A list of differences currently found in the tree</param> /// <param name="currentDepth">The current depth of the tree recursion</param> /// <param name="maxDepth">The maximum number of tree children to recurse</param> /// <param name="objectTree">A hash table containing the tree that has already been traversed, to prevent recursion loops</param> /// <param name="options">Specify the comparison options</param> /// <param name="propertyList">A list of property names or full path names to ignore</param> /// <returns></returns> private List <Difference> GetDifferences(string propertyName, string showName, Type propertyType, TypeConverter typeConverter, object left, object right, object parent, List <Difference> differences, int currentDepth, int maxDepth, ObjectHashcodeMap objectTree, string path, ComparisonOptions options, List <string> propertyList) { if (GetPropertyInclusionState(propertyName, path, options, propertyList, null) == FilterResult.Exclude) { return(differences); } object leftValue = null; object rightValue = null; leftValue = left; if (options.BitwiseHasFlag(ComparisonOptions.AllowCompareDifferentObjects) && rightValue != null) { rightValue = GetValueForProperty(right, propertyName); } else { rightValue = right; } if (rightValue == null && leftValue != null || leftValue == null && rightValue != null) { differences.Add(new Difference((leftValue ?? rightValue).GetType(), propertyName, path, leftValue, rightValue, typeConverter)); return(differences); } if (leftValue == null && rightValue == null) { return(differences); } var propertyTypeSupport = propertyType.GetExtendedType(DefaultTypeSupportOptions); var isCollection = propertyType != typeof(string) && propertyType.GetInterface(nameof(IEnumerable)) != null; if (isCollection && options.BitwiseHasFlag(ComparisonOptions.CompareCollections)) { var genericArguments = propertyType.GetGenericArguments(); var isArray = propertyTypeSupport.IsArray; var elementType = propertyTypeSupport.ElementType; // iterate the collection var aValueCollection = (leftValue as IEnumerable); var bValueCollection = (rightValue as IEnumerable); var bValueCollectionCount = GetCountFromEnumerable(bValueCollection); var bValueEnumerator = bValueCollection?.GetEnumerator(); if (aValueCollection != null) { if (!options.BitwiseHasFlag(ComparisonOptions.AllowCollectionsToBeOutOfOrder)) { var arrayIndex = 0; // compare elements must be the same order foreach (var collectionItem in aValueCollection) { var hasValue = bValueEnumerator?.MoveNext() ?? false; leftValue = collectionItem; if (hasValue) { rightValue = bValueEnumerator?.Current; if (leftValue == null && rightValue == null) { continue; } if (leftValue == null && rightValue != null) { differences.Add(new Difference(rightValue?.GetType() ?? elementType, propertyName, showName, path, arrayIndex, leftValue, rightValue, typeConverter)); } // check array element for difference if (leftValue != null && !leftValue.GetType().IsValueType&& leftValue.GetType() != typeof(string)) { differences = RecurseProperties(leftValue, rightValue, parent, differences, currentDepth, maxDepth, objectTree, path, options, propertyList); } else if (leftValue != null && leftValue.GetType().IsGenericType&& leftValue.GetType().GetGenericTypeDefinition() == typeof(KeyValuePair <,>)) { // compare keys and values of a KVP var leftKvpKey = GetValueForProperty(leftValue, "Key"); var leftKvpValue = GetValueForProperty(leftValue, "Value"); var rightKvpKey = GetValueForProperty(rightValue, "Key"); var rightKvpValue = GetValueForProperty(rightValue, "Value"); Type leftKeyType = leftKvpKey?.GetType() ?? genericArguments.First(); Type leftValueType = leftKvpValue?.GetType() ?? genericArguments.Skip(1).First(); // compare the key if (leftKvpKey != null && !leftKeyType.IsValueType && leftKeyType != typeof(string)) { differences = RecurseProperties(leftKvpKey, rightKvpKey, leftValue, differences, currentDepth, maxDepth, objectTree, path, options, propertyList); } else { if (!IsMatch(leftKvpKey, rightKvpKey)) { differences.Add(new Difference(leftKeyType, propertyName, path, showName, arrayIndex, leftKvpKey, rightKvpKey, typeConverter)); } } // compare the value if (leftKvpValue != null && !leftValueType.IsValueType && leftValueType != typeof(string)) { differences = RecurseProperties(leftKvpValue, rightKvpValue, leftValue, differences, currentDepth, maxDepth, objectTree, path, options, propertyList); } else { if (!IsMatch(leftValue, rightValue)) { differences.Add(new Difference(leftValueType, propertyName, path, showName, arrayIndex, leftKvpValue, rightKvpValue, typeConverter)); } } } else { if (!IsMatch(leftValue, rightValue)) { differences.Add(new Difference(leftValue?.GetType() ?? elementType, propertyName, showName, path, arrayIndex, leftValue, rightValue, typeConverter)); } } } else { // left has a value in collection, right does not. That's a difference rightValue = null; differences.Add(new Difference(leftValue?.GetType() ?? elementType, propertyName, showName, path, arrayIndex, leftValue, rightValue, typeConverter)); } arrayIndex++; } if (bValueCollectionCount > arrayIndex) { // right side has extra elements var rightSideExtraElements = bValueCollectionCount - arrayIndex; if (bValueEnumerator != null) { for (var i = 0; i < rightSideExtraElements; i++) { var hasValue = bValueEnumerator?.MoveNext() ?? false; if (hasValue) { differences.Add(new Difference(aValueCollection.GetType(), propertyName, showName, path, arrayIndex, null, bValueEnumerator.Current, typeConverter)); arrayIndex++; } } } } } else { // compare elements can be of different order (use the hashcode for this) var leftHashCodeEntryList = new Dictionary <int, CollectionKey>(); // hashcode, value/count var rightHashCodeEntryList = new Dictionary <int, CollectionKey>(); // hashcode, value/count var arrayIndex = 0; foreach (var collectionItem in aValueCollection) { var hashCode = collectionItem.GetHashCode(); if (leftHashCodeEntryList.ContainsKey(hashCode)) { var val = leftHashCodeEntryList[hashCode]; val.Matches++; val.OriginalIndex = arrayIndex; // populate the highest value } else { leftHashCodeEntryList.Add(hashCode, new CollectionKey(arrayIndex, collectionItem, 1)); } arrayIndex++; } arrayIndex = 0; foreach (var collectionItem in bValueCollection) { var hashCode = collectionItem.GetHashCode(); if (rightHashCodeEntryList.ContainsKey(hashCode)) { var val = rightHashCodeEntryList[hashCode]; val.Matches++; val.OriginalIndex = arrayIndex; // populate the highest value } else { rightHashCodeEntryList.Add(hashCode, new CollectionKey(arrayIndex, collectionItem, 1)); } arrayIndex++; } var orderedLeft = leftHashCodeEntryList.OrderBy(x => x.Key); var orderedRight = rightHashCodeEntryList.OrderBy(x => x.Key); // compare the left collection foreach (var item in orderedLeft) { var matchFound = orderedRight.Where(x => x.Key == item.Key).Select(x => x.Value).FirstOrDefault(); if (matchFound == null) { differences.Add(new Difference((leftValue ?? rightValue).GetType(), propertyName, showName, path, item.Value.OriginalIndex, item.Value.Value, null, typeConverter)); } else { var isEqual = CompareForObjectEquality(item.Value.Value.GetExtendedType(DefaultTypeSupportOptions), item.Value.Value, matchFound.Value); if (!isEqual) { differences.Add(new Difference((leftValue ?? rightValue).GetType(), propertyName, showName, path, item.Value.OriginalIndex, item.Value.Value, null, typeConverter)); } else if (matchFound.Matches != item.Value.Matches && item.Value.Matches > matchFound.Matches) { // also compare that the number of instances of the value in a collection are the same differences.Add(new Difference((leftValue ?? rightValue).GetType(), propertyName, showName, path, item.Value.OriginalIndex, item.Value.Value, null, typeConverter)); } } } // compare the right collection foreach (var item in orderedRight) { var matchFound = orderedLeft.Where(x => x.Key == item.Key).Select(x => x.Value).FirstOrDefault(); if (matchFound == null) { differences.Add(new Difference((leftValue ?? rightValue).GetType(), propertyName, showName, path, item.Value.OriginalIndex, null, item.Value.Value, typeConverter)); } else { var isEqual = CompareForObjectEquality(item.Value.Value.GetExtendedType(DefaultTypeSupportOptions), item.Value.Value, matchFound.Value); if (!isEqual) { differences.Add(new Difference((leftValue ?? rightValue).GetType(), propertyName, showName, path, item.Value.OriginalIndex, null, item.Value.Value, typeConverter)); } else if (matchFound.Matches != item.Value.Matches && item.Value.Matches > matchFound.Matches) { // also compare that the number of instances of the value in a collection are the same differences.Add(new Difference((leftValue ?? rightValue).GetType(), propertyName, showName, path, item.Value.OriginalIndex, null, item.Value.Value, typeConverter)); } } } } } } else if (!propertyType.IsValueType && propertyType != typeof(string)) { differences = RecurseProperties(leftValue, rightValue, leftValue, differences, currentDepth, maxDepth, objectTree, path, options, propertyList); } else { if (!IsMatch(leftValue, rightValue)) { differences.Add(new Difference(propertyType, propertyName, showName, path, leftValue, rightValue, typeConverter)); } } return(differences); }
/// <summary> /// Recurse the object's tree /// </summary> /// <param name="left">The left object to compare</param> /// <param name="right">The right object to compare</param> /// <param name="parent">The parent object</param> /// <param name="differences">A list of differences currently found in the tree</param> /// <param name="currentDepth">The current depth of the tree recursion</param> /// <param name="maxDepth">The maximum number of tree children to recurse</param> /// <param name="objectTree">A hash table containing the tree that has already been traversed, to prevent recursion loops</param> /// <param name="comparisonOptions">Specify the comparison options</param> /// <param name="diffOptions">Specify custom diff options</param> /// <param name="propertiesToExcludeOrInclude">A list of property names or full path names to include/exclude. Default is <seealso cref="ComparisonOptions.ExcludeList"/>. Specify <seealso cref="ComparisonOptions.ExcludeList"/> to exclude the specified properties from the Diff or <seealso cref="ComparisonOptions.IncludeList"/> to only Diff properties contained in the list.</param> /// <param name="path">The current path</param> /// <returns></returns> private List <Difference> RecurseProperties(object left, object right, object parent, List <Difference> differences, int currentDepth, int maxDepth, ObjectHashcodeMap objectTree, string path, ComparisonOptions comparisonOptions, ICollection <string> propertiesToExcludeOrInclude, DiffOptions diffOptions) { if (!comparisonOptions.BitwiseHasFlag(ComparisonOptions.AllowCompareDifferentObjects) && left != null && right != null && left?.GetType() != right?.GetType()) { throw new ArgumentException("Objects Left and Right must be of the same type."); } if (left == null && right == null) { return(differences); } if (maxDepth > 0 && currentDepth >= maxDepth) { return(differences); } var type = left != null?left.GetType() : right.GetType(); var typeSupport = type.GetExtendedType(DefaultTypeSupportOptions); if (typeSupport.Attributes.Any(x => diffOptions.AttributeIgnoreList.Contains(x))) { return(differences); } if (typeSupport.IsDelegate) { return(differences); } if (comparisonOptions.BitwiseHasFlag(ComparisonOptions.AllowEqualsOverride)) { // if the object has a custom equality comparitor, use its output (isObjectEqual) instead of iterating the object. // if it does not have one specified, this method will return false if (CompareForObjectEquality(typeSupport, left, right, out var isObjectEqual) && isObjectEqual) { return(differences); // no differences found, no need to continue } } // increment the current recursion depth currentDepth++; // construct a hashtable of objects we have already inspected (simple recursion loop preventer) // we use this hashcode method as it does not use any custom hashcode handlers the object might implement if (left != null) { if (objectTree?.Contains(left) == true) { return(differences); } objectTree?.Add(left); } // get list of properties var properties = new List <ExtendedProperty>(); if (comparisonOptions.BitwiseHasFlag(ComparisonOptions.CompareProperties)) { properties.AddRange(left.GetProperties(PropertyOptions.All)); } // get all fields, except for backed auto-property fields var fields = new List <ExtendedField>(); if (comparisonOptions.BitwiseHasFlag(ComparisonOptions.CompareFields)) { fields.AddRange(left.GetFields(FieldOptions.All)); fields = fields.Where(x => !x.IsBackingField).ToList(); } var rootPath = path; var localPath = string.Empty; foreach (var property in properties) { localPath = $"{rootPath}.{property.Name}"; object leftValue = null; try { if (left != null) { leftValue = left.GetPropertyValue(property); } } catch (Exception) { // catch any exceptions accessing the property } object rightValue = null; try { if (right != null) { rightValue = right.GetPropertyValue(property); } } catch (Exception) { // catch any exceptions accessing the property } differences = GetDifferences(property.Name, property.Type, GetTypeConverter(property), property.CustomAttributes, leftValue, rightValue, parent, differences, currentDepth, maxDepth, objectTree, localPath, comparisonOptions, propertiesToExcludeOrInclude, diffOptions); } foreach (var field in fields) { localPath = $"{rootPath}.{field.Name}"; object leftValue = null; if (left != null) { leftValue = left.GetFieldValue(field); } object rightValue = null; if (right != null) { rightValue = right.GetFieldValue(field); } differences = GetDifferences(field.Name, field.Type, GetTypeConverter(field), field.CustomAttributes, leftValue, rightValue, parent, differences, currentDepth, maxDepth, objectTree, localPath, comparisonOptions, propertiesToExcludeOrInclude, diffOptions); } return(differences); }
/// <summary> /// Recurse the object's tree /// </summary> /// <param name="left">The left object to compare</param> /// <param name="right">The right object to compare</param> /// <param name="parent">The parent object</param> /// <param name="differences">A list of differences currently found in the tree</param> /// <param name="currentDepth">The current depth of the tree recursion</param> /// <param name="maxDepth">The maximum number of tree children to recurse</param> /// <param name="objectTree">A hash table containing the tree that has already been traversed, to prevent recursion loops</param> /// <param name="options">Specify the comparison options</param> /// <param name="propertyList">A list of property names or full path names to ignore</param> /// <returns></returns> private List <Difference> RecurseProperties(object left, object right, object parent, List <Difference> differences, int currentDepth, int maxDepth, ObjectHashcodeMap objectTree, string path, ComparisonOptions options, List <string> propertyList) { if (GetPropertyInclusionState(null, path, options, propertyList, null) == FilterResult.Exclude) { return(differences); } if (!options.BitwiseHasFlag(ComparisonOptions.AllowCompareDifferentObjects) && left != null && right != null && left?.GetType() != right?.GetType()) { throw new ArgumentException("Objects Left and Right must be of the same type."); } if (left == null && right == null) { return(differences); } if (maxDepth > 0 && currentDepth >= maxDepth) { return(differences); } var typeSupport = new ExtendedType(left != null ? left.GetType() : right.GetType(), DefaultTypeSupportOptions); if (typeSupport.Attributes.Any(x => _ignoreAttributes.Contains(x))) { return(differences); } if (typeSupport.IsDelegate) { return(differences); } if (options.BitwiseHasFlag(ComparisonOptions.AllowEqualsOverride)) { var objectEquality = CompareForObjectEquality(typeSupport, left, right); if (objectEquality) { return(differences); // no differences found, no need to continue } } // increment the current recursion depth currentDepth++; // construct a hashtable of objects we have already inspected (simple recursion loop preventer) // we use this hashcode method as it does not use any custom hashcode handlers the object might implement if (left != null) { if (objectTree.Contains(left)) { return(differences); } objectTree.Add(left); } // get list of properties var properties = new List <ExtendedProperty>(); if (options.BitwiseHasFlag(ComparisonOptions.CompareProperties)) { var pis = left.GetProperties(PropertyOptions.All); foreach (var pi in pis) { if (pi.PropertyInfo.CanWrite == false || pi.PropertyInfo.CanRead == false) { continue; } var atts = pi.PropertyInfo.GetCustomAttributes(true); var canAdd = true; foreach (var att in atts) { var name = att.GetType().Name.ToLower(); if (name.Contains("ignore") || name.Contains("result")) { canAdd = false; break; } } if (canAdd) { properties.Add(pi); } } } // get all fields, except for backed auto-property fields var fields = new List <ExtendedField>(); if (options.BitwiseHasFlag(ComparisonOptions.CompareFields)) { var fis = left.GetFields(FieldOptions.All); foreach (var fi in fis) { if (fi.IsBackingField) { continue; } var atts = fi.FieldInfo.GetCustomAttributes(true); var canAdd = true; foreach (var att in atts) { var name = att.GetType().Name.ToLower(); if (name.Contains("ignore") || name.Contains("result") || name.Contains("nonserialized")) { canAdd = false; break; } } if (canAdd) { fields.Add(fi); } } } var rootPath = path; var localPath = string.Empty; foreach (var property in properties) { localPath = $"{rootPath}.{property.Name}"; if (GetPropertyInclusionState(property.Name, localPath, options, propertyList, property.CustomAttributes) == FilterResult.Exclude) { continue; } object leftValue = null; try { if (left != null) { leftValue = left.GetPropertyValue(property); } } catch (Exception) { // catch any exceptions accessing the property } object rightValue = null; try { if (right != null) { rightValue = right.GetPropertyValue(property); } } catch (Exception) { // catch any exceptions accessing the property } var showName = property.Name; var attr = property.PropertyInfo.GetCustomAttribute <DisplayNameAttribute>(true); if (attr != null) { showName = attr.DisplayName; } differences = GetDifferences(property.Name, showName, property.Type, GetTypeConverter(property), leftValue, rightValue, parent, differences, currentDepth, maxDepth, objectTree, localPath, options, propertyList); } foreach (var field in fields) { localPath = $"{rootPath}.{field.Name}"; if (GetPropertyInclusionState(field.Name, localPath, options, propertyList, field.CustomAttributes) == FilterResult.Exclude) { continue; } object leftValue = null; if (left != null) { leftValue = left.GetFieldValue(field); } object rightValue = null; if (right != null) { rightValue = right.GetFieldValue(field); } var showName = field.Name; var attr = field.FieldInfo.GetCustomAttribute <DisplayNameAttribute>(true); if (attr != null) { showName = attr.DisplayName; } differences = GetDifferences(field.Name, showName, field.Type, GetTypeConverter(field), leftValue, rightValue, parent, differences, currentDepth, maxDepth, objectTree, localPath, options, propertyList); } return(differences); }