/// <summary> /// Compare two objects for value differences /// </summary> /// <typeparam name="T"></typeparam> /// <param name="left"></param> /// <param name="right"></param> /// <param name="comparisonOptions">Specify the comparison options</param> /// <param name="diffOptions">Specify custom diff options</param> /// <param name="propertiesToExcludeOrInclude"></param> /// <returns></returns> public List <Difference> ComputeDiff <T>(T left, T right, ComparisonOptions comparisonOptions, DiffOptions diffOptions, params Expression <Func <T, object> >[] propertiesToExcludeOrInclude) { return(ComputeDiff(left, right, DefaultMaxDepth, comparisonOptions, diffOptions, propertiesToExcludeOrInclude)); }
/// <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> /// 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> /// Compare two objects for value differences /// </summary> /// <param name="left"></param> /// <param name="right"></param> /// <param name="maxDepth"></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> /// <returns></returns> public List <Difference> ComputeDiff(object left, object right, int maxDepth, ComparisonOptions comparisonOptions, DiffOptions diffOptions, params string[] propertiesToExcludeOrInclude) { return(RecurseProperties(left, right, null, new List <Difference>(), 0, maxDepth, new ObjectHashcodeMap(), string.Empty, comparisonOptions, propertiesToExcludeOrInclude, diffOptions)); }
/// <summary> /// Compare two objects for value differences /// </summary> /// <typeparam name="T"></typeparam> /// <param name="left">Object A</param> /// <param name="right">Object B</param> /// <param name="maxDepth">Maximum recursion depth</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> /// <returns></returns> public List <Difference> ComputeDiff <T>(T left, T right, int maxDepth, ComparisonOptions comparisonOptions, DiffOptions diffOptions, params Expression <Func <T, object> >[] propertiesToExcludeOrInclude) { var ignorePropertiesList = new List <string>(); var expressionManager = new ExpressionManager(); if (propertiesToExcludeOrInclude != null) { foreach (var expression in propertiesToExcludeOrInclude) { var name = expressionManager.GetPropertyPath(expression.Body); ignorePropertiesList.Add(name); } } return(RecurseProperties(left, right, null, new List <Difference>(), 0, maxDepth, new ObjectHashcodeMap(), string.Empty, comparisonOptions, ignorePropertiesList, diffOptions)); }
/// <summary> /// Compare two objects for value differences /// </summary> /// <typeparam name="T"></typeparam> /// <param name="left">Object A</param> /// <param name="right">Object B</param> /// <param name="comparisonOptions">Specify the comparison options</param> /// <param name="diffOptions">Specify custom diff options</param> /// <returns></returns> public static ICollection <Difference> Diff <T>(T left, T right, ComparisonOptions comparisonOptions, DiffOptions diffOptions) { var diffProvider = new DiffProvider(); return(diffProvider.ComputeDiff(left, right, DiffProvider.DefaultMaxDepth, comparisonOptions, diffOptions)); }
/// <summary> /// Compare two objects for value differences /// </summary> /// <param name="left">Object A</param> /// <param name="right">Object B</param> /// <param name="diffOptions">Specify custom diff options</param> /// <returns></returns> public static ICollection <Difference> Diff(object left, object right, DiffOptions diffOptions) { var diffProvider = new DiffProvider(); return(diffProvider.ComputeDiff(left, right, DiffProvider.DefaultMaxDepth, ComparisonOptions.All, diffOptions)); }
/// <summary> /// Compare two objects for value differences /// </summary> /// <typeparam name="T"></typeparam> /// <param name="left">Object A</param> /// <param name="right">Object B</param> /// <param name="maxDepth">Maximum recursion depth</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> /// <returns></returns> public static ICollection <Difference> Diff <T>(T left, T right, int maxDepth, ComparisonOptions comparisonOptions, DiffOptions diffOptions, params Expression <Func <T, object> >[] propertiesToExcludeOrInclude) { var diffProvider = new DiffProvider(); return(diffProvider.ComputeDiff(left, right, maxDepth, comparisonOptions, diffOptions, propertiesToExcludeOrInclude)); }
/// <summary> /// Compare two objects for value differences /// </summary> /// <param name="left">Object A</param> /// <param name="right">Object B</param> /// <param name="maxDepth">Maximum recursion depth</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> /// <returns></returns> public static ICollection <Difference> Diff(object left, object right, int maxDepth, ComparisonOptions comparisonOptions, DiffOptions diffOptions, params string[] propertiesToExcludeOrInclude) { var diffProvider = new DiffProvider(); return(diffProvider.ComputeDiff(left, right, maxDepth, comparisonOptions, diffOptions, propertiesToExcludeOrInclude)); }
/// <summary> /// Compare two objects for value differences /// </summary> /// <typeparam name="T"></typeparam> /// <param name="left">Object A</param> /// <param name="right">Object B</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> /// <returns></returns> public static ICollection <Difference> Diff <T>(T left, T right, ComparisonOptions comparisonOptions, DiffOptions diffOptions, string[] propertiesToExcludeOrInclude) { var diffProvider = new DiffProvider(); return(diffProvider.ComputeDiff(left, right, DiffProvider.DefaultMaxDepth, comparisonOptions, diffOptions, propertiesToExcludeOrInclude)); }