/// <summary> /// Test if the all values of objects properties are equals recursively /// </summary> /// <param name="expected">The expected object</param> /// <param name="actual">Actual object</param> /// <param name="processedElements"> /// Table that binds a managed object, which is represented by a key, to its attached property, which is represented by a value. /// Automatically removes the key/value entry as soon as no other references to a key exist outside the table. /// </param> /// <param name="result">For which property equality was broken</param> /// <returns></returns> private static bool AreDeeplyEqual( object expected, object actual, ConditionalWeakTable <object, object> processedElements, DeepEqualityResult result) { result.ApplyValues(expected, actual); if (expected == null && actual == null) { return(result.Success); } if (expected == null || actual == null) { return(result.Failure); } var expectedType = expected.GetType(); if (expectedType != typeof(string) && !expectedType.GetTypeInfo().IsValueType) { if (processedElements.TryGetValue(expected, out _)) { return(result.Success); } processedElements.Add(expected, expected); } var actualType = actual.GetType(); var objectType = typeof(object); if ((expectedType == objectType && actualType != objectType) || (actualType == objectType && expectedType != objectType)) { return(result.Failure); } var stringType = typeof(string); if (expected is IEnumerable && expectedType != stringType) { return(CollectionsAreDeeplyEqual(expected, actual, processedElements, result)); } var expectedTypeIsAnonymous = IsAnonymousType(expectedType); if (expectedTypeIsAnonymous) { var actualIsAnonymous = IsAnonymousType(actualType); if (!actualIsAnonymous) { return(result.Failure); } } if (!expectedTypeIsAnonymous && expectedType != actualType && !expectedType.IsAssignableFrom(actualType) && !actualType.IsAssignableFrom(expectedType)) { return(result.Failure); } if (expectedType.GetTypeInfo().IsPrimitive || expectedType.GetTypeInfo().IsEnum) { return(expected.ToString() == actual.ToString() ? result.Success : result.Failure); } var equalsOperator = expectedType.GetMethods().FirstOrDefault(m => m.Name == "op_Equality"); if (equalsOperator != null) { var equalsOperatorResult = (bool)equalsOperator.Invoke(null, new[] { expected, actual }); if (!equalsOperatorResult && expectedType != stringType) { result.PushPath("== (Equality Operator)"); if (!expectedType.IsDateTimeRelated()) { result.ClearValues(); } } return(equalsOperatorResult ? result.Success : result.Failure); } if (expectedType != objectType && !expectedTypeIsAnonymous) { var equalsMethod = expectedType.GetMethods() .FirstOrDefault(m => m.Name == "Equals" && m.DeclaringType == expectedType); if (equalsMethod != null) { var equalsMethodResult = (bool)equalsMethod.Invoke(expected, new[] { actual }); if (!equalsMethodResult) { result .PushPath("Equals()") .ClearValues(); } return(equalsMethodResult ? result.Success : result.Failure); } } if (ComparablesAreDeeplyEqual(expected, actual, result)) { return(result.Success); } if (!ObjectPropertiesAreDeeplyEqual(expected, actual, processedElements, result)) { return(false); } return(true); }