diff --git a/ObjectsComparer/ObjectsComparer.Examples/Example4/Example4Tests.cs b/ObjectsComparer/ObjectsComparer.Examples/Example4/Example4Tests.cs index 0d7125e..28125f4 100644 --- a/ObjectsComparer/ObjectsComparer.Examples/Example4/Example4Tests.cs +++ b/ObjectsComparer/ObjectsComparer.Examples/Example4/Example4Tests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using ObjectsComparer.Exceptions; using static ObjectsComparer.Examples.OutputHelper; // ReSharper disable PossibleMultipleEnumeration @@ -65,6 +66,48 @@ public void List_Of_Equal_Sizes_But_Is_Inequality() Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Instruction" && d.Value1 == "Instruction 1" && d.Value2 == "Instruction One")); } + [Test] + public void CalculateDifferenceTree_Throw_DifferenceBuilderNotImplemented() + { + var formula1 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 60, + Name = "Item 1", + Instruction = "Instruction 1" + } + } + }; + + var formula2 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 80, + Name = "Item One", + Instruction = "Instruction One" + } + } + }; + + Assert.Throws(() => + { + var rootNode = _comparer.CalculateDifferenceTree(formula1, formula2); + var differences = rootNode.GetDifferences(true).ToArray(); + }); + } + [Test] public void List_Of_Different_Sizes_But_Is_Inequality() { diff --git a/ObjectsComparer/ObjectsComparer.Examples/Example4/Example4Tests_BuiltInKeyComparison.cs b/ObjectsComparer/ObjectsComparer.Examples/Example4/Example4Tests_BuiltInKeyComparison.cs new file mode 100644 index 0000000..5cc1996 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Examples/Example4/Example4Tests_BuiltInKeyComparison.cs @@ -0,0 +1,306 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using static ObjectsComparer.Examples.OutputHelper; + +// ReSharper disable PossibleMultipleEnumeration +namespace ObjectsComparer.Examples.Example4 +{ + [TestFixture] + public class Example4Tests_BuiltInKeyComparison + { + [Test] + public void List_Of_Equal_Sizes_But_Is_Inequality() + { + var formula1 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 60, + Name = "Item 1", + Instruction = "Instruction 1" + } + } + }; + + var formula2 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 80, + Name = "Item One", + Instruction = "Instruction One" + } + } + }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey: true); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(formula1, formula2, out var differences); + + ResultToOutput(isEqual, differences); + + Assert.IsFalse(isEqual); + Assert.AreEqual(3, differences.Count()); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[1].Delay" && d.Value1 == "60" && d.Value2 == "80")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[1].Name" && d.Value1 == "Item 1" && d.Value2 == "Item One")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[1].Instruction" && d.Value1 == "Instruction 1" && d.Value2 == "Instruction One")); + } + + [Test] + public void List_Of_Equal_Sizes_But_Is_Inequality_FormatKey() + { + var formula1 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 60, + Name = "Item 1", + Instruction = "Instruction 1" + } + } + }; + + var formula2 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 80, + Name = "Item One", + Instruction = "Instruction One" + } + } + }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(listOptions => listOptions + .CompareElementsByKey(keyOptions => keyOptions + .FormatElementKey(formatKeyArgs => $"Id={formatKeyArgs.ElementKey}"))); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(formula1, formula2, out var differences); + + ResultToOutput(isEqual, differences); + + Assert.IsFalse(isEqual); + Assert.AreEqual(3, differences.Count()); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Delay" && d.Value1 == "60" && d.Value2 == "80")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Name" && d.Value1 == "Item 1" && d.Value2 == "Item One")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Instruction" && d.Value1 == "Instruction 1" && d.Value2 == "Instruction One")); + } + + + [Test] + public void List_Of_Different_Sizes_But_Is_Inequality() + { + var formula1 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 60, + Name = "Item 1", + Instruction = "Instruction 1" + } + } + }; + + var formula2 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 80, + Name = "Item One", + Instruction = "Instruction One" + }, + new FormulaItem + { + Id = 2, + Delay = 30, + Name = "Item Two", + Instruction = "Instruction Two" + } + } + }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey: true, compareUnequalLists: true); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(formula1, formula2, out var differences); + + ResultToOutput(isEqual, differences); + + Assert.IsFalse(isEqual); + Assert.AreEqual(5, differences.Count()); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items" && d.Value1 == "1" && d.Value2 == "2" && d.DifferenceType == DifferenceTypes.NumberOfElementsMismatch)); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[1].Delay" && d.Value1 == "60" && d.Value2 == "80")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[1].Name" && d.Value1 == "Item 1" && d.Value2 == "Item One")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[1].Instruction" && d.Value1 == "Instruction 1" && d.Value2 == "Instruction One")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[2]" && d.DifferenceType == DifferenceTypes.MissedElementInFirstObject && d.Value1 == "" && d.Value2 == "ObjectsComparer.Examples.Example4.FormulaItem")); + } + + [Test] + public void List_Of_Different_Sizes_But_Is_Inequality_FormatKey() + { + var formula1 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 60, + Name = "Item 1", + Instruction = "Instruction 1" + } + } + }; + + var formula2 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 80, + Name = "Item One", + Instruction = "Instruction One" + }, + new FormulaItem + { + Id = 2, + Delay = 30, + Name = "Item Two", + Instruction = "Instruction Two" + } + } + }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(listOptions => listOptions + .CompareUnequalLists(true) + .CompareElementsByKey(keyOptions => keyOptions + .FormatElementKey(formatKeyArgs => $"Id={formatKeyArgs.ElementKey}"))); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(formula1, formula2, out var differences); + + ResultToOutput(isEqual, differences); + + Assert.IsFalse(isEqual); + Assert.AreEqual(5, differences.Count()); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items" && d.Value1 == "1" && d.Value2 == "2" && d.DifferenceType == DifferenceTypes.NumberOfElementsMismatch)); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Delay" && d.Value1 == "60" && d.Value2 == "80")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Name" && d.Value1 == "Item 1" && d.Value2 == "Item One")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Instruction" && d.Value1 == "Instruction 1" && d.Value2 == "Instruction One")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.MissedElementInFirstObject && d.Value1 == "" && d.Value2 == "ObjectsComparer.Examples.Example4.FormulaItem")); + } + + [Test] + public void List_Of_Different_Sizes_But_Is_Inequality_FormatKey_CheckDifferenceTreeNode() + { + var formula1 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 60, + Name = "Item 1", + Instruction = "Instruction 1" + } + } + }; + + var formula2 = new Formula + { + Id = 1, + Name = "Formula 1", + Items = new List + { + new FormulaItem + { + Id = 1, + Delay = 80, + Name = "Item One", + Instruction = "Instruction One" + }, + new FormulaItem + { + Id = 2, + Delay = 30, + Name = "Item Two", + Instruction = "Instruction Two" + } + } + }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(listOptions => listOptions + .CompareUnequalLists(true) + .CompareElementsByKey(keyOptions => keyOptions + .FormatElementKey(formatKeyArgs => $"Id={formatKeyArgs.ElementKey}"))); + + var comparer = new Comparer(settings); + var rootDifferenceNode = comparer.CalculateDifferenceTree(formula1, formula2); + var differences = rootDifferenceNode.GetDifferences(recursive: true).ToArray(); + bool isEqual = differences.Any() == false; + ResultToOutput(isEqual, differences); + + Assert.IsFalse(isEqual); + Assert.AreEqual(5, differences.Count()); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items" && d.Value1 == "1" && d.Value2 == "2" && d.DifferenceType == DifferenceTypes.NumberOfElementsMismatch)); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Delay" && d.Value1 == "60" && d.Value2 == "80")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Name" && d.Value1 == "Item 1" && d.Value2 == "Item One")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Items[Id=1].Instruction" && d.Value1 == "Instruction 1" && d.Value2 == "Instruction One")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.MissedElementInFirstObject && d.Value1 == "" && d.Value2 == "ObjectsComparer.Examples.Example4.FormulaItem")); + } + } +} \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer.Tests/ComparerTests.cs b/ObjectsComparer/ObjectsComparer.Tests/ComparerTests.cs index bf55943..b324e6b 100644 --- a/ObjectsComparer/ObjectsComparer.Tests/ComparerTests.cs +++ b/ObjectsComparer/ObjectsComparer.Tests/ComparerTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Threading; using NSubstitute; using NUnit.Framework; using ObjectsComparer.Tests.TestClasses; @@ -67,8 +69,11 @@ public void ReadOnlyPropertyInequality() CollectionAssert.IsNotEmpty(differences); Assert.AreEqual("ReadOnlyProperty", differences.First().MemberPath); - Assert.AreEqual("1.99", differences.First().Value1); - Assert.AreEqual("0.89", differences.First().Value2); + //Assert.AreEqual("1.99", differences.First().Value1); + //Assert.AreEqual("0.89", differences.First().Value2); + NumberFormatInfo nfi = CultureInfo.CurrentCulture.NumberFormat; + Assert.AreEqual($"1{nfi.NumberDecimalSeparator}99", differences.First().Value1); + Assert.AreEqual($"0{nfi.NumberDecimalSeparator}89", differences.First().Value2); } [Test] diff --git a/ObjectsComparer/ObjectsComparer.Tests/Comparer_CompilerGeneratedObjectsTests.cs b/ObjectsComparer/ObjectsComparer.Tests/Comparer_CompilerGeneratedObjectsTests.cs index 3128f7a..268e694 100644 --- a/ObjectsComparer/ObjectsComparer.Tests/Comparer_CompilerGeneratedObjectsTests.cs +++ b/ObjectsComparer/ObjectsComparer.Tests/Comparer_CompilerGeneratedObjectsTests.cs @@ -1,6 +1,7 @@ using System.Linq; using NUnit.Framework; using NSubstitute; +using System.Collections.Generic; namespace ObjectsComparer.Tests { @@ -34,6 +35,32 @@ public void DifferentValues() Assert.IsTrue(differences.Any(d => d.MemberPath == "Field3" && d.Value1 == "True" && d.Value2 == "False")); } + [Test] + public void DifferentValues_CalculateDifferenceTree() + { + dynamic a1 = new + { + Field1 = "A", + Field2 = 5, + Field3 = true + }; + dynamic a2 = new + { + Field1 = "B", + Field2 = 8, + Field3 = false + }; + var comparer = new Comparer(); + + var rootNode = comparer.CalculateDifferenceTree(typeof(object), (object)a1, (object)a2); + var differences = rootNode.GetDifferences().ToList(); + + Assert.AreEqual(3, differences.Count); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Field1" && d.Value1 == "A" && d.Value2 == "B")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Field2" && d.Value1 == "5" && d.Value2 == "8")); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Field3" && d.Value1 == "True" && d.Value2 == "False")); + } + [Test] public void MissedFields() { @@ -60,6 +87,33 @@ public void MissedFields() Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.MissedMemberInFirstObject && d.MemberPath == "Field3" && d.Value2 == "False")); } + [Test] + public void MissedFields_CheckDifferenceTreeNode() + { + dynamic a1 = new + { + Field1 = "A", + Field2 = 5 + }; + + dynamic a2 = new + { + Field1 = "B", + Field2 = 8, + Field3 = false + }; + + var comparer = new Comparer(); + + var rootDiffenenceNode = comparer.CalculateDifferenceTree(typeof(object), (object)a1, (object)a2); + var calculateDifferences = rootDiffenenceNode.GetDifferences(true).ToArray(); + + Assert.AreEqual(3, calculateDifferences.Count()); + Assert.IsTrue(calculateDifferences.Any(d => d.MemberPath == "Field1" && d.Value1 == "A" && d.Value2 == "B")); + Assert.IsTrue(calculateDifferences.Any(d => d.MemberPath == "Field2" && d.Value1 == "5" && d.Value2 == "8")); + Assert.IsTrue(calculateDifferences.Any(d => d.DifferenceType == DifferenceTypes.MissedMemberInFirstObject && d.MemberPath == "Field3" && d.Value2 == "False")); + } + [Test] public void MissedFieldsAndUseDefaults() { @@ -103,6 +157,28 @@ public void Hierarchy() Assert.IsTrue(differences.Any(d => d.MemberPath == "Field1.FieldSub1" && d.Value1 == "10" && d.Value2 == "8")); } + [Test] + public void Hierarchy_CalculateDifferenceTree() + { + dynamic a1Sub1 = new { FieldSub1 = 10 }; + dynamic a1 = new { Field1 = a1Sub1 }; + dynamic a2Sub1 = new { FieldSub1 = 8 }; + dynamic a2 = new { Field1 = a2Sub1 }; + var comparer = new Comparer(); + + var rootNode = comparer.CalculateDifferenceTree(typeof(object), (object)a1, (object)a2); + var differences = rootNode.GetDifferences().ToList(); + + Assert.AreEqual(1, differences.Count); + Assert.IsTrue(differences.Any(d => d.MemberPath == "Field1.FieldSub1" && d.Value1 == "10" && d.Value2 == "8")); + + var field1 = rootNode.Descendants.First(); + var fieldSub1 = field1.Descendants.First(); + + Assert.IsTrue(differences.Any(d => d.MemberPath == $"{field1.Member.Name}.{fieldSub1.Member.Name}" && d.Value1 == "10" && d.Value2 == "8")); + Assert.IsTrue(differences.Any(d => d.MemberPath == $"{field1.Member.Info.Name}.{fieldSub1.Member.Info.Name}" && d.Value1 == "10" && d.Value2 == "8")); + } + [Test] public void DifferentTypes() { @@ -171,6 +247,35 @@ public void NullAndMissedMemberAreNotEqual() d => d.MemberPath == "Field2" && d.DifferenceType == DifferenceTypes.MissedMemberInFirstObject)); } + [Test] + public void NullAndMissedMemberAreNotEqual_CheckDifferenceTreeNode() + { + dynamic a1 = new + { + Field1 = (object)null + }; + dynamic a2 = new + { + Field2 = (object)null + }; + + var t = (a1 as object).GetType(); + var members = t.GetMembers(); + + var comparer = new Comparer(); + + + var rootDifferenceNode = comparer.CalculateDifferenceTree(typeof(object), (object)a1, (object)a2); + var calculateDifferences = rootDifferenceNode.GetDifferences(true); + + Assert.IsTrue(calculateDifferences.Any()); + Assert.AreEqual(2, calculateDifferences.Count()); + Assert.IsTrue(calculateDifferences.Any( + d => d.MemberPath == "Field1" && d.DifferenceType == DifferenceTypes.MissedMemberInSecondObject)); + Assert.IsTrue(calculateDifferences.Any( + d => d.MemberPath == "Field2" && d.DifferenceType == DifferenceTypes.MissedMemberInFirstObject)); + } + [Test] public void NullValues() { diff --git a/ObjectsComparer/ObjectsComparer.Tests/Comparer_ExpandoObjectsTests.cs b/ObjectsComparer/ObjectsComparer.Tests/Comparer_ExpandoObjectsTests.cs index f286784..2c47e42 100644 --- a/ObjectsComparer/ObjectsComparer.Tests/Comparer_ExpandoObjectsTests.cs +++ b/ObjectsComparer/ObjectsComparer.Tests/Comparer_ExpandoObjectsTests.cs @@ -4,6 +4,8 @@ using System.Dynamic; using Newtonsoft.Json; using NSubstitute; +using System.Diagnostics.CodeAnalysis; +using ObjectsComparer.Tests.Utils; namespace ObjectsComparer.Tests { @@ -98,6 +100,31 @@ public void Hierarchy() Assert.IsTrue(differences.Any(d => d.MemberPath == "FieldSub1.Field1" && d.Value1 == "10" && d.Value2 == "8")); } + [Test] + public void Hierarchy_CheckDifferenceTreeNode() + { + dynamic a1Sub1 = new ExpandoObject(); + a1Sub1.Field1 = 10; + dynamic a1 = new ExpandoObject(); + a1.FieldSub1 = a1Sub1; + dynamic a2Sub1 = new ExpandoObject(); + a2Sub1.Field1 = 8; + dynamic a2 = new ExpandoObject(); + a2.FieldSub1 = a2Sub1; + var comparer = new Comparer(); + + var isEqual = comparer.Compare(a1, a2, out IEnumerable differencesEnum); + var differences = differencesEnum.ToList(); + + Assert.IsFalse(isEqual); + Assert.AreEqual(1, differences.Count); + Assert.IsTrue(differences.Any(d => d.MemberPath == "FieldSub1.Field1" && d.Value1 == "10" && d.Value2 == "8")); + + var rootNode = comparer.CalculateDifferenceTree(typeof(object), (object)a1, (object)a2); + var treeDifferences = rootNode.GetDifferences(true).ToList(); + Assert.IsTrue(treeDifferences.Any(d => d.MemberPath == "FieldSub1.Field1" && d.Value1 == "10" && d.Value2 == "8")); + } + [Test] public void DifferentTypes() { @@ -255,6 +282,27 @@ public void UseDefaultValuesWhenSubclassNotSpecified() Assert.IsTrue(isEqual); } + [Test] + public void UseDefaultValuesWhenSubclassNotSpecified_CheckDifferenceTreeNode() + { + dynamic a1 = new ExpandoObject(); + a1.Field1 = new ExpandoObject(); + a1.Field1.SubField1 = 0; + a1.Field1.SubField2 = null; + a1.Field1.SubField3 = 0.0; + dynamic a2 = new ExpandoObject(); + var comparer = new Comparer(new ComparisonSettings { UseDefaultIfMemberNotExist = true }); + + var obja1 = (object)a1; + var obja2 = (object)a2; + + var rootNode = comparer.CalculateDifferenceTree(((object)a1).GetType(), obja1, obja2); + IEnumerable diffs = rootNode.GetDifferences(true); + var differences = diffs.ToArray(); + + CollectionAssert.IsEmpty(differences); + } + [Test] public void DifferenceWhenSubclassNotSpecified() { @@ -275,6 +323,36 @@ public void DifferenceWhenSubclassNotSpecified() d => d.MemberPath == "Field1" && d.DifferenceType == DifferenceTypes.MissedMemberInSecondObject)); } + [Test] + public void DifferenceWhenSubclassNotSpecified_CheckDifferenceTreeNode() + { + dynamic a1 = new ExpandoObject(); + a1.Field1 = new ExpandoObject(); + a1.Field1.SubField1 = 0; + a1.Field1.SubField2 = null; + a1.Field1.SubField3 = 0.0; + dynamic a2 = new ExpandoObject(); + var comparer = new Comparer(); + + var isEqual = comparer.Compare(a1, a2, out IEnumerable differencesEnum); + var compareDifferences = differencesEnum.ToList(); + + Assert.IsFalse(isEqual); + Assert.AreEqual(1, compareDifferences.Count); + Assert.IsTrue(compareDifferences.Any( + d => d.MemberPath == "Field1" && d.DifferenceType == DifferenceTypes.MissedMemberInSecondObject)); + + var obja1 = (object)a1; + var obja2 = (object)a2; + + var rootNode = comparer.CalculateDifferenceTree(((object)a1).GetType(), obja1, obja2); + var calculateDifferences = rootNode.GetDifferences(true).ToList(); + + Assert.AreEqual(1, calculateDifferences.Count); + Assert.IsTrue(calculateDifferences.Any( + d => d.MemberPath == "Field1" && d.DifferenceType == DifferenceTypes.MissedMemberInSecondObject)); + } + [Test] public void ExpandoObjectWithCollections() { @@ -294,5 +372,28 @@ public void ExpandoObjectWithCollections() Assert.IsTrue(differences.Any( d => d.MemberPath == "Transaction[0].No" && d.DifferenceType == DifferenceTypes.ValueMismatch)); } + + [Test] + public void ExpandoObjectWithCollectionCheckDifferenceTreeNodePath() + { + var comparer = new Comparer(new ComparisonSettings { RecursiveComparison = true }); + + dynamic a1 = JsonConvert.DeserializeObject( + "{ \"Transaction\": [ { \"Name\": \"abc\", \"No\": 101 } ] }"); + + dynamic a2 = JsonConvert.DeserializeObject( + "{ \"Transaction\": [ { \"Name\": \"abc\", \"No\": 102 } ] }"); + + var rootNode = comparer.CalculateDifferenceTree(typeof(object), (object)a1, (object)a2); + var namePropertyPath = $"{rootNode.Descendants.First().Member.Name}.[0].{rootNode.Descendants.First().Descendants.First().Descendants.First().Member.Name}"; + var noPropertyPath = $"{rootNode.Descendants.First().Member.Name}.[0].{rootNode.Descendants.First().Descendants.First().Descendants.Skip(1).First().Member.Name}"; + var differences = rootNode.GetDifferences(true).ToList(); + + Assert.AreEqual(1, differences.Count); + Assert.IsTrue(differences.Any( + d => d.MemberPath == "Transaction[0].No" && d.DifferenceType == DifferenceTypes.ValueMismatch)); + Assert.AreEqual("Transaction.[0].Name", namePropertyPath); + Assert.AreEqual("Transaction.[0].No", noPropertyPath); + } } } diff --git a/ObjectsComparer/ObjectsComparer.Tests/Comparer_GenericEnumerableTests.cs b/ObjectsComparer/ObjectsComparer.Tests/Comparer_GenericEnumerableTests.cs index b14c185..2128bb0 100644 --- a/ObjectsComparer/ObjectsComparer.Tests/Comparer_GenericEnumerableTests.cs +++ b/ObjectsComparer/ObjectsComparer.Tests/Comparer_GenericEnumerableTests.cs @@ -3,6 +3,18 @@ using NUnit.Framework; using ObjectsComparer.Tests.TestClasses; using System.Collections.Generic; +using ObjectsComparer.Exceptions; +using System; +using ObjectsComparer.Utils; +using ObjectsComparer.Tests.Utils; +using System.Diagnostics; +using Newtonsoft.Json; +using System.Text; +using System.Collections; +using System.Reflection; +using System.IO; +using System.ComponentModel; +using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; namespace ObjectsComparer.Tests { @@ -21,6 +33,23 @@ public void ValueTypeArrayEquality() Assert.IsTrue(isEqual); } + [Test] + public void ValueTypeArrayEquality_CompareByKey() + { + var a1 = new A { IntArray = new[] { 2, 1 } }; + var a2 = new A { IntArray = new[] { 1, 2 } }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(a1, a2); + + Assert.IsTrue(isEqual); + } + [Test] public void PrimitiveTypeArrayInequalityCount() { @@ -37,6 +66,33 @@ public void PrimitiveTypeArrayInequalityCount() Assert.AreEqual("3", differences[0].Value2); } + [Test] + public void PrimitiveTypeArrayInequalityCount_CompareUnequalLists() + { + var a1 = new A { IntArray = new[] { 1, 2 } }; + var a2 = new A { IntArray = new[] { 1, 2, 3 } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareUnequalLists: true); + + var comparer = new Comparer(settings); + + var rootNode = comparer.CalculateDifferenceTree(a1, a2); + var differences = rootNode.GetDifferences(true).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[0].DifferenceType); + Assert.AreEqual("IntArray[2]", differences[0].MemberPath); + Assert.AreEqual(string.Empty, differences[0].Value1); + Assert.AreEqual("3", differences[0].Value2); + + Assert.AreEqual("IntArray.Length", differences[1].MemberPath); + Assert.AreEqual("2", differences[1].Value1); + Assert.AreEqual("3", differences[1].Value2); + } + [Test] public void PrimitiveTypeArrayInequalityMember() { @@ -52,6 +108,65 @@ public void PrimitiveTypeArrayInequalityMember() Assert.AreEqual("3", differences.First().Value2); } + [Test] + public void PrimitiveTypeArrayInequalityMember_CompareByKey() + { + var a1 = new A { IntArray = new[] { 1, 2 } }; + var a2 = new A { IntArray = new[] { 1, 3 } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.IsTrue(differences.Count == 2); + + var diff1 = differences[0]; + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, diff1.DifferenceType); + Assert.AreEqual("IntArray[2]", diff1.MemberPath); + Assert.AreEqual("2", diff1.Value1); + Assert.AreEqual(string.Empty, diff1.Value2); + + var diff2 = differences[1]; + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, diff2.DifferenceType); + Assert.AreEqual("IntArray[3]", diff2.MemberPath); + Assert.AreEqual(string.Empty, diff2.Value1); + Assert.AreEqual("3", diff2.Value2); + } + + [Test] + public void PrimitiveTypeArrayInequalityMember_CompareByKey_FormatKey() + { + var a1 = new A { IntArray = new[] { 1, 2 } }; + var a2 = new A { IntArray = new[] { 1, 3 } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => + { + listOptions.CompareElementsByKey(keyOptions => keyOptions.FormatElementKey(args => $"Key={args.ElementKey}")); + }); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.IsTrue(differences.Count == 2); + + var diff1 = differences[0]; + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, diff1.DifferenceType); + Assert.AreEqual("IntArray[Key=2]", diff1.MemberPath); + Assert.AreEqual("2", diff1.Value1); + Assert.AreEqual(string.Empty, diff1.Value2); + + var diff2 = differences[1]; + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, diff2.DifferenceType); + Assert.AreEqual("IntArray[Key=3]", diff2.MemberPath); + Assert.AreEqual(string.Empty, diff2.Value1); + Assert.AreEqual("3", diff2.Value2); + } + [Test] public void PrimitiveTypeArrayInequalityFirstNull() { @@ -68,6 +183,25 @@ public void PrimitiveTypeArrayInequalityFirstNull() Assert.AreEqual(DifferenceTypes.ValueMismatch, differences.First().DifferenceType); } + [Test] + public void PrimitiveTypeArrayInequalityFirstNull_CompareBykey() + { + var a1 = new A(); + var a2 = new A { IntArray = new int[0] }; + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual("IntArray", differences.First().MemberPath); + Assert.AreEqual(string.Empty, differences.First().Value1); + Assert.AreEqual(a2.IntArray.ToString(), differences.First().Value2); + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences.First().DifferenceType); + } + [Test] public void PrimitiveTypeArrayInequalitySecondNull() { @@ -83,6 +217,25 @@ public void PrimitiveTypeArrayInequalitySecondNull() Assert.AreEqual(string.Empty, differences.First().Value2); } + [Test] + public void PrimitiveTypeArrayInequalitySecondNull_CompareByKey() + { + var a1 = new A { IntArray = new int[0] }; + var a2 = new A(); + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual("IntArray", differences.First().MemberPath); + Assert.AreEqual(a1.IntArray.ToString(), differences.First().Value1); + Assert.AreEqual(string.Empty, differences.First().Value2); + } + [Test] public void ClassArrayEquality() { @@ -95,6 +248,43 @@ public void ClassArrayEquality() Assert.IsTrue(isEqual); } + [Test] + public void ClassArrayEquality_ComareByKey_Throw_ElementKeyNotFoundException() + { + var a1 = new A { ArrayOfB = new[] { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + var a2 = new A { ArrayOfB = new[] { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + Assert.Throws(() => + { + var isEqual = comparer.Compare(a1, a2); + }); + } + + [Test] + public void ClassArrayEquality_ComareByKey_DoesNotThrow_ElementKeyNotFoundException() + { + var a1 = new A { ArrayOfB = new[] { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + var a2 = new A { ArrayOfB = new[] { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey(keyOptions => keyOptions.ThrowKeyNotFound(false))); + + var comparer = new Comparer(settings); + bool isEqual = false; + + Assert.DoesNotThrow(() => + { + isEqual = comparer.Compare(a1, a2); + }); + + Assert.IsTrue(isEqual); + } + [Test] public void ClassArrayInequalityCount() { @@ -111,6 +301,65 @@ public void ClassArrayInequalityCount() Assert.AreEqual("2", differences[0].Value2); } + [Test] + public void ClassArrayInequalityCount_CompareByKey_DoesNotThrow_ElementKeyNotFoundException() + { + var a1 = new A { ArrayOfB = new[] { new B { Property1 = "Str1" } } }; + var a2 = new A { ArrayOfB = new[] { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(listOptions => listOptions + .CompareUnequalLists(true) + .CompareElementsByKey(keyOptions => keyOptions.ThrowKeyNotFound(false))); + + settings.ConfigureListComparison((currentNode, options) => + { + if (currentNode.Member.Name == "TrvaleAdresy") + { + options.CompareElementsByKey(); + } + }); + + var comparer = new Comparer(settings); + + List differences = null; + + Assert.DoesNotThrow(() => differences = comparer.CalculateDifferences(a1, a2).ToList()); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(1, differences.Count); + Assert.AreEqual("ArrayOfB.Length", differences[0].MemberPath); + Assert.AreEqual("1", differences[0].Value1); + Assert.AreEqual("2", differences[0].Value2); + } + + [Test] + public void ClassArrayInequalityCount_CompareUnequalLists() + { + var a1 = new A { ArrayOfB = new[] { new B { Property1 = "Str1" } } }; + var a2 = new A { ArrayOfB = new[] { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true)); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[0].DifferenceType); + Assert.AreEqual("ArrayOfB[1]", differences[0].MemberPath); + Assert.AreEqual(string.Empty, differences[0].Value1); + Assert.AreEqual("ObjectsComparer.Tests.TestClasses.B", differences[0].Value2); + + Assert.AreEqual("ArrayOfB.Length", differences[1].MemberPath); + Assert.AreEqual("1", differences[1].Value1); + Assert.AreEqual("2", differences[1].Value2); + } + [Test] public void ClassArrayInequalityProperty() { @@ -126,6 +375,47 @@ public void ClassArrayInequalityProperty() Assert.AreEqual("Str3", differences.First().Value2); } + [Test] + public void ClassArrayInequalityProperty_CompareByKey() + { + var a1 = new A { ArrayOfB = new[] { new B { Property1 = "Str2", Id = 1 }, new B { Property1 = "Str1", Id = 2 } } }; + var a2 = new A { ArrayOfB = new[] { new B { Property1 = "Str1", Id = 2 }, new B { Property1 = "Str3", Id = 1 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual("ArrayOfB[1].Property1", differences.First().MemberPath); + Assert.AreEqual("Str2", differences.First().Value1); + Assert.AreEqual("Str3", differences.First().Value2); + } + + [Test] + public void ClassArrayInequalityProperty_CompareByKey_FormatKey() + { + var a1 = new A { ArrayOfB = new[] { new B { Property1 = "Str2", Id = 2 }, new B { Property1 = "Str1", Id = 1 } } }; + var a2 = new A { ArrayOfB = new[] { new B { Property1 = "Str1", Id = 1 }, new B { Property1 = "Str3", Id = 2 } } }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(listOptions => + listOptions + .CompareElementsByKey(keyOptions => keyOptions.FormatElementKey(args => $"Id={args.ElementKey}"))); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(1, differences.Count); + Assert.AreEqual("ArrayOfB[Id=2].Property1", differences.First().MemberPath); + Assert.AreEqual("Str2", differences.First().Value1); + Assert.AreEqual("Str3", differences.First().Value2); + } + [Test] public void CollectionEquality() { @@ -138,6 +428,22 @@ public void CollectionEquality() Assert.IsTrue(isEqual); } + [Test] + public void CollectionEquality_CompareByKey() + { + var a1 = new A { CollectionOfB = new Collection { new B { Property1 = "Str2", Id = 1 }, new B { Property1 = "Str1", Id = 2 } } }; + var a2 = new A { CollectionOfB = new Collection { new B { Property1 = "Str1", Id = 2 }, new B { Property1 = "Str2", Id = 1 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(a1, a2); + + Assert.IsTrue(isEqual); + } + [Test] public void CollectionInequalityCount() { @@ -155,6 +461,33 @@ public void CollectionInequalityCount() Assert.AreEqual("1", differences[0].Value2); } + [Test] + public void CollectionInequalityCount_CompareUnequalLists() + { + var a1 = new A { CollectionOfB = new Collection { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + var a2 = new A { CollectionOfB = new Collection { new B { Property1 = "Str1" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true)); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences[0].DifferenceType); + Assert.AreEqual("CollectionOfB", differences[0].MemberPath); + Assert.AreEqual("2", differences[0].Value1); + Assert.AreEqual("1", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, differences[1].DifferenceType); + Assert.AreEqual("CollectionOfB[1]", differences[1].MemberPath); + Assert.AreEqual("ObjectsComparer.Tests.TestClasses.B", differences[1].Value1); + Assert.AreEqual("", differences[1].Value2); + } + [Test] public void CollectionAndNullInequality() { @@ -171,6 +504,29 @@ public void CollectionAndNullInequality() Assert.AreEqual(string.Empty, differences[0].Value2); } + [Test] + public void CollectionAndNullInequality_CompareByKey() + { + var a1 = new A { CollectionOfB = new Collection { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + var a2 = new A(); + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => + { + listOptions.CompareElementsByKey(); + }); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(1, differences.Count); + Assert.AreEqual("CollectionOfB", differences[0].MemberPath); + Assert.AreEqual(a1.CollectionOfB.ToString(), differences[0].Value1); + Assert.AreEqual(string.Empty, differences[0].Value2); + } + [Test] public void NullAndCollectionInequality() { @@ -187,6 +543,29 @@ public void NullAndCollectionInequality() Assert.AreEqual(a2.CollectionOfB.ToString(), differences[0].Value2); } + [Test] + public void NullAndCollectionInequality_CompareByKey() + { + var a1 = new A(); + var a2 = new A { CollectionOfB = new Collection { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => + { + listOptions.CompareElementsByKey(); + }); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(1, differences.Count); + Assert.AreEqual("CollectionOfB", differences[0].MemberPath); + Assert.AreEqual(string.Empty, differences[0].Value1); + Assert.AreEqual(a2.CollectionOfB.ToString(), differences[0].Value2); + } + [Test] public void IgnoreAttributeComparisonEquality() { @@ -363,6 +742,25 @@ public void CollectionInequalityProperty() Assert.AreEqual("Str3", differences.First().Value2); } + [Test] + public void CollectionInequalityProperty_CompareByKey() + { + var a1 = new A { CollectionOfB = new Collection { new B { Property1 = "Str2", Id = 1 }, new B { Property1 = "Str1", Id = 2 } } }; + var a2 = new A { CollectionOfB = new Collection { new B { Property1 = "Str1", Id = 2 }, new B { Property1 = "Str3", Id = 1 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual("CollectionOfB[1].Property1", differences.First().MemberPath); + Assert.AreEqual("Str2", differences.First().Value1); + Assert.AreEqual("Str3", differences.First().Value2); + } + [Test] public void ClassImplementsCollectionEquality() { @@ -375,6 +773,22 @@ public void ClassImplementsCollectionEquality() Assert.IsTrue(isEqual); } + [Test] + public void ClassImplementsCollectionEquality_CompareByKey() + { + var a1 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1", Id = 1 }, new B { Property1 = "Str2", Id = 2 } } }; + var a2 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str2", Id = 2 }, new B { Property1 = "Str1", Id = 1 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(a1, a2); + + Assert.IsTrue(isEqual); + } + [Test] public void ClassImplementsCollectionInequalityCount() { @@ -393,11 +807,65 @@ public void ClassImplementsCollectionInequalityCount() } [Test] - public void ClassImplementsCollectionInequalityProperty() + public void ClassImplementsCollectionInequalityCount_CompareUnequalLists() { var a1 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; - var a2 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1" }, new B { Property1 = "Str3" } } }; - var comparer = new Comparer(); + var a2 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true)); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences[0].DifferenceType); + Assert.AreEqual("ClassImplementsCollectionOfB", differences[0].MemberPath); + Assert.AreEqual("2", differences[0].Value1); + Assert.AreEqual("1", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, differences[1].DifferenceType); + Assert.AreEqual("ClassImplementsCollectionOfB[1]", differences[1].MemberPath); + Assert.AreEqual("ObjectsComparer.Tests.TestClasses.B", differences[1].Value1); + Assert.AreEqual("", differences[1].Value2); + } + + [Test] + public void ClassImplementsCollectionInequalityCount_CompareUnequalLists_CompareByKey() + { + var a1 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1", Id = 1 }, new B { Property1 = "Str2", Id = 2 } } }; + var a2 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1", Id = 1 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true).CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences[0].DifferenceType); + Assert.AreEqual("ClassImplementsCollectionOfB", differences[0].MemberPath); + Assert.AreEqual("2", differences[0].Value1); + Assert.AreEqual("1", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, differences[1].DifferenceType); + Assert.AreEqual("ClassImplementsCollectionOfB[2]", differences[1].MemberPath); + Assert.AreEqual("ObjectsComparer.Tests.TestClasses.B", differences[1].Value1); + Assert.AreEqual("", differences[1].Value2); + } + + [Test] + public void ClassImplementsCollectionInequalityProperty() + { + var a1 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + var a2 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1" }, new B { Property1 = "Str3" } } }; + var comparer = new Comparer(); var differences = comparer.CalculateDifferences(a1, a2).ToList(); @@ -407,6 +875,25 @@ public void ClassImplementsCollectionInequalityProperty() Assert.AreEqual("Str3", differences.First().Value2); } + [Test] + public void ClassImplementsCollectionInequalityProperty_CompareByKey() + { + var a1 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1", Id = 1 }, new B { Property1 = "Str2", Id = 2 } } }; + var a2 = new A { ClassImplementsCollectionOfB = new CollectionOfB { new B { Property1 = "Str1", Id = 1 }, new B { Property1 = "Str3", Id = 2 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true).CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual("ClassImplementsCollectionOfB[2].Property1", differences.First().MemberPath); + Assert.AreEqual("Str2", differences.First().Value1); + Assert.AreEqual("Str3", differences.First().Value2); + } + [Test] public void NullAndEmptyComparisonGenericInequality() { @@ -495,6 +982,70 @@ public void CollectionOfBCountInequality1() Assert.AreEqual("2", differences.First().Value2); } + [Test] + public void CollectionOfBCountInequality1_CompareElementsByKey() + { + var a1 = new A + { + EnumerableOfB = new[] { new B { Property1 = "B1" } } + }; + var a2 = new A + { + EnumerableOfB = new[] { new B { Property1 = "B1" }, new B { Property1 = "B2" } } + }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(a1, a2, out var differencesEnum); + var differences = differencesEnum.ToList(); + + Assert.IsFalse(isEqual); + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(1, differences.Count); + Assert.AreEqual("EnumerableOfB", differences.First().MemberPath); + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + Assert.AreEqual("1", differences.First().Value1); + Assert.AreEqual("2", differences.First().Value2); + } + + [Test] + public void CollectionOfBCountInequality1_CompareElementsByKey_CompareUnequalLists() + { + var a1 = new A + { + EnumerableOfB = new[] { new B { Property1 = "B1", Id = 1 } } + }; + var a2 = new A + { + EnumerableOfB = new[] { new B { Property1 = "B1", Id = 1 }, new B { Property1 = "B2", Id = 2 } } + }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true).CompareElementsByKey()); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(a1, a2, out var differencesEnum); + var differences = differencesEnum.ToList(); + + Assert.IsFalse(isEqual); + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual("EnumerableOfB", differences.First().MemberPath); + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + Assert.AreEqual("1", differences.First().Value1); + Assert.AreEqual("2", differences.First().Value2); + + Assert.AreEqual("EnumerableOfB[2]", differences[1].MemberPath); + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("", differences[1].Value1); + Assert.AreEqual("ObjectsComparer.Tests.TestClasses.B", differences[1].Value2); + } + [Test] public void CollectionOfBCountInequality2() { @@ -505,7 +1056,7 @@ public void CollectionOfBCountInequality2() var a2 = new A { EnumerableOfB = new B[0] - }; + }; var comparer = new Comparer(); var isEqual = comparer.Compare(a1, a2, out var differencesEnum); @@ -520,6 +1071,39 @@ public void CollectionOfBCountInequality2() Assert.AreEqual("0", differences.First().Value2); } + [Test] + public void CollectionOfBCountInequality2_CompareByKey() + { + var a1 = new A + { + EnumerableOfB = new[] { new B { Property1 = "B1", Id = 1 } } + }; + var a2 = new A + { + EnumerableOfB = new B[0] + }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true).CompareElementsByKey()); + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(a1, a2, out var differencesEnum); + var differences = differencesEnum.ToList(); + + Assert.IsFalse(isEqual); + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual("EnumerableOfB", differences.First().MemberPath); + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + Assert.AreEqual("1", differences.First().Value1); + Assert.AreEqual("0", differences.First().Value2); + + Assert.AreEqual("EnumerableOfB[1]", differences[1].MemberPath); + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, differences[1].DifferenceType); + Assert.AreEqual("ObjectsComparer.Tests.TestClasses.B", differences[1].Value1); + Assert.AreEqual("", differences[1].Value2); + } [Test] public void HashSetEqualitySameOrder() @@ -635,6 +1219,182 @@ public void CompareAsIList() Assert.AreEqual("1", differences.First().Value2); } + [Test] + public void PrimitiveTypeArray_CompareByKey_CompareUnequalLists_Ignore_Repeated_Elements() + { + var a1 = new A() { IntArray = new int[] { 1, 2 } }; + var a2 = new A() { IntArray = new int[] { 1, 2, 1, 2, 1, 2, 1, 2, 1, 2 } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey().CompareUnequalLists(true)); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.IsTrue(differences.Count == 1); + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[0].DifferenceType); + Assert.AreEqual("IntArray.Length", differences[0].MemberPath); + Assert.AreEqual("2", differences[0].Value1); + Assert.AreEqual("10", differences[0].Value2); + } + + [Test] + public void PrimitiveTypeArray_CompareByIndex_CompareUnequalLists_Dont_Ignore_Repeated_Elements() + { + var a1 = new A() { IntArray = new int[] { 1, 2 } }; + var a2 = new A() { IntArray = new int[] { 1, 2, 1, 2, 1, 2, 1, 2, 1, 2 } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true)); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.IsTrue(differences.Count == 9); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[0].DifferenceType); + Assert.AreEqual("IntArray[2]", differences[0].MemberPath); + Assert.AreEqual("", differences[0].Value1); + Assert.AreEqual("1", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("IntArray[3]", differences[1].MemberPath); + Assert.AreEqual("", differences[1].Value1); + Assert.AreEqual("2", differences[1].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[2].DifferenceType); + Assert.AreEqual("IntArray[4]", differences[2].MemberPath); + Assert.AreEqual("", differences[2].Value1); + Assert.AreEqual("1", differences[2].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[3].DifferenceType); + Assert.AreEqual("IntArray[5]", differences[3].MemberPath); + Assert.AreEqual("", differences[3].Value1); + Assert.AreEqual("2", differences[3].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[4].DifferenceType); + Assert.AreEqual("IntArray[6]", differences[4].MemberPath); + Assert.AreEqual("", differences[4].Value1); + Assert.AreEqual("1", differences[4].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[5].DifferenceType); + Assert.AreEqual("IntArray[7]", differences[5].MemberPath); + Assert.AreEqual("", differences[5].Value1); + Assert.AreEqual("2", differences[5].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[6].DifferenceType); + Assert.AreEqual("IntArray[8]", differences[6].MemberPath); + Assert.AreEqual("", differences[6].Value1); + Assert.AreEqual("1", differences[6].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[7].DifferenceType); + Assert.AreEqual("IntArray[9]", differences[7].MemberPath); + Assert.AreEqual("", differences[7].Value1); + Assert.AreEqual("2", differences[7].Value2); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[8].DifferenceType); + Assert.AreEqual("IntArray.Length", differences[8].MemberPath); + Assert.AreEqual("2", differences[8].Value1); + Assert.AreEqual("10", differences[8].Value2); + } + + [Test] + public void CompareAsIList_CompareUnequalLists() + { + var list1 = new List { 1, 2 }; + var list2 = new List { 1 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true)); + + var comparer = new Comparer>(settings); + + var differences = comparer.CalculateDifferences(list1, list2).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + Assert.AreEqual("2", differences.First().Value1); + Assert.AreEqual("1", differences.First().Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, differences[1].DifferenceType); + Assert.AreEqual("[1]", differences[1].MemberPath); + Assert.AreEqual("2", differences[1].Value1); + Assert.AreEqual(string.Empty, differences[1].Value2); + } + + [Test] + public void CompareAsIList_CompareUnequalLists_CompareElementsByKey() + { + var list1 = new List { 1, 2 }; + var list2 = new List { 1 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true).CompareElementsByKey()); + + var comparer = new Comparer>(settings); + + var differences = comparer.CalculateDifferences(list1, list2).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + Assert.AreEqual("2", differences.First().Value1); + Assert.AreEqual("1", differences.First().Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, differences[1].DifferenceType); + Assert.AreEqual("[2]", differences[1].MemberPath); + Assert.AreEqual("2", differences[1].Value1); + Assert.AreEqual(string.Empty, differences[1].Value2); + } + + [Test] + public void CompareAsIList_CompareUnequalLists_CompareElementsByKey_FormatKey() + { + var list1 = new List { 1, 2 }; + var list2 = new List { 1 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => + { + //DaN Fluent. + //listOptions.CompareUnequalLists().CompareElementsByKey(keyOptions => keyOptions.FormatElementKey(elementKey => $"Key={elementKey}")); + + /* + * listOptions + * .CompareUnequalLists() + * .CompareElementsByKey(keyOptions => keyOptions.FormatElementKey(elementKey => $"Key={elementKey}")); + */ + + listOptions.CompareUnequalLists(true); + + listOptions.CompareElementsByKey(keyOptions => + { + keyOptions.FormatElementKey(args => + { + return $"Key={args.ElementKey}"; + }); + }); + }); + + var comparer = new Comparer>(settings); + + var differences = comparer.CalculateDifferences(list1, list2).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + Assert.AreEqual("2", differences.First().Value1); + Assert.AreEqual("1", differences.First().Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, differences[1].DifferenceType); + Assert.AreEqual("[Key=2]", differences[1].MemberPath); + Assert.AreEqual("2", differences[1].Value1); + Assert.AreEqual(string.Empty, differences[1].Value2); + } + [Test] public void DictionaryEqualitySameOrder() { @@ -647,6 +1407,23 @@ public void DictionaryEqualitySameOrder() Assert.IsTrue(isEqual); } + [Test] + public void DictionaryEqualitySameOrder_CompareByKey() + { + var a1 = new Dictionary { { 1, "One" }, { 2, "Two" } }; + var a2 = new Dictionary { { 1, "One" }, { 2, "Two" } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer>(settings); + + var isEqual = comparer.Compare(a1, a2, out var differences); + var diffs = differences.ToArray(); + + Assert.IsTrue(isEqual); + } + [Test] public void DictionaryInequalityDifferentOrder() { @@ -659,6 +1436,22 @@ public void DictionaryInequalityDifferentOrder() Assert.IsFalse(isEqual); } + [Test] + public void DictionaryEqualityDifferentOrder_CompareByKey() + { + var a1 = new Dictionary { { 1, "One" }, { 2, "Two" } }; + var a2 = new Dictionary { { 2, "Two" }, { 1, "One" } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer>(settings); + + var isEqual = comparer.Compare(a1, a2); + + Assert.IsTrue(isEqual); + } + [Test] public void DictionaryInequalityDifferentNumberOfElements() { @@ -672,6 +1465,100 @@ public void DictionaryInequalityDifferentNumberOfElements() Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); } + [Test] + public void DictionaryInequalityDifferentNumberOfElements_CompareByKey() + { + var a1 = new Dictionary { { 1, "One" }, { 2, "Two" } }; + var a2 = new Dictionary { { 1, "One" }, { 2, "Two" }, { 3, "Three" } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer>(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(1, differences.Count); + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + } + + [Test] + public void DictionaryInequalityDifferentNumberOfElements_CompareByKey_CompareUnequalLists() + { + var a1 = new Dictionary { { 1, "One" }, { 2, "Two" } }; + var a2 = new Dictionary { { 1, "One" }, { 2, "Two" }, { 3, "Three" } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey: true, compareUnequalLists: true); + + var comparer = new Comparer>(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences[0].DifferenceType); + Assert.AreEqual("2", differences[0].Value1); + Assert.AreEqual("3", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("[3]", differences[1].MemberPath); + Assert.AreEqual("", differences[1].Value1); + Assert.AreEqual("[3, Three]", differences[1].Value2); + } + + [Test] + public void DictionaryInequalityDifferentNumberOfElements_CompareByKey_CompareUnequalLists_FormatKey() + { + var a1 = new Dictionary { { 1, "One" }, { 2, "Two" } }; + var a2 = new Dictionary { { 1, "One" }, { 2, "Two" }, { 3, "Three" } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions + .CompareUnequalLists(true) + .CompareElementsByKey(keyOptions => keyOptions.FormatElementKey(keyArgs => $"Key={keyArgs.ElementKey}"))); + + var comparer = new Comparer>(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences[0].DifferenceType); + Assert.AreEqual("2", differences[0].Value1); + Assert.AreEqual("3", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("[Key=3]", differences[1].MemberPath); + Assert.AreEqual("", differences[1].Value1); + Assert.AreEqual("[3, Three]", differences[1].Value2); + } + + [Test] + public void DictionaryInequalityDifferentNumberOfElements_CompareByIndex_CompareUnequalLists() + { + var a1 = new Dictionary { { 1, "One" }, { 2, "Two" } }; + var a2 = new Dictionary { { 1, "One" }, { 2, "Two" }, { 3, "Three" } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey: false, compareUnequalLists: true); + + var comparer = new Comparer>(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences[0].DifferenceType); + Assert.AreEqual("2", differences[0].Value1); + Assert.AreEqual("3", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("[2]", differences[1].MemberPath); + Assert.AreEqual("", differences[1].Value1); + Assert.AreEqual("[3, Three]", differences[1].Value2); + } + [Test] public void DictionaryInequalityDifferentValue() { @@ -687,5 +1574,423 @@ public void DictionaryInequalityDifferentValue() Assert.AreEqual("Two!", differences.First().Value2); Assert.AreEqual("[1].Value", differences.First().MemberPath); } + + [Test] + public void DictionaryInequalityDifferentValue_CompareByKey() + { + var a1 = new Dictionary { { 1, "One" }, { 2, "Two" } }; + var a2 = new Dictionary { { 1, "One" }, { 2, "Two!" } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey()); + + var comparer = new Comparer>(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(1, differences.Count); + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences.First().DifferenceType); + Assert.AreEqual("Two", differences.First().Value1); + Assert.AreEqual("Two!", differences.First().Value2); + Assert.AreEqual("[2].Value", differences.First().MemberPath); + } + + [Test] + public void DictionaryInequalityDifferentValue_CompareByKey_FormatElementKey() + { + var a1 = new Dictionary { { 1, "One" }, { 2, "Two" } }; + var a2 = new Dictionary { { 1, "One" }, { 2, "Two!" } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions + .CompareElementsByKey(keyOptions => keyOptions.FormatElementKey(args => $"Key={args.ElementKey}"))); + + var comparer = new Comparer>(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(1, differences.Count); + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences.First().DifferenceType); + Assert.AreEqual("Two", differences.First().Value1); + Assert.AreEqual("Two!", differences.First().Value2); + Assert.AreEqual("[Key=2].Value", differences.First().MemberPath); + } + + [Test] + public void CompareIntArrayDefaultBehavior() + { + var a1 = new int[] { 3, 2, 1 }; + var a2 = new int[] { 1, 2, 3, 4 }; + + var comparer = new Comparer(); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 1); + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[0].DifferenceType); + Assert.AreEqual("Length", differences[0].MemberPath); + Assert.AreEqual("3", differences[0].Value1); + Assert.AreEqual("4", differences[0].Value2); + } + + [Test] + public void CompareIntArrayUnequalListEnabled() + { + var a1 = new int[] { 3, 2, 1 }; + var a2 = new int[] { 1, 2, 3, 4 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareUnequalLists: true); + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 4); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "[0]" && d.Value1 == "3" && d.Value2 == "1")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "[2]" && d.Value1 == "1" && d.Value2 == "3")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.MissedElementInFirstObject && d.MemberPath == "[3]" && d.Value1 == "" && d.Value2 == "4")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "Length" && d.Value1 == "3" && d.Value2 == "4")); + } + + [Test] + public void CompareIntArrayByKey() + { + var a1 = new int[] { 3, 2, 1 }; + var a2 = new int[] { 1, 2, 3, 4 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey: true); + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 1); + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[0].DifferenceType); + Assert.AreEqual("Length", differences[0].MemberPath); + Assert.AreEqual("3", differences[0].Value1); + Assert.AreEqual("4", differences[0].Value2); + } + + [Test] + public void CompareIntArrayByKey_UnequalListEnabled() + { + var a1 = new int[] { 3, 2, 1 }; + var a2 = new int[] { 1, 2, 3, 4 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey: true, compareUnequalLists: true); + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 2); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.MissedElementInFirstObject && d.MemberPath == "[4]" && d.Value1 == "" && d.Value2 == "4")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "Length" && d.Value1 == "3" && d.Value2 == "4")); + } + + [Test] + public void CompareIntArrayByKeyDisplayIndex() + { + var a1 = new int[] { 3, 2, 1 }; + var a2 = new int[] { 1, 2, 3, 4 }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(listOptions => + { + listOptions + .CompareUnequalLists(true) + .CompareElementsByKey(keyOptions => + { + keyOptions.FormatElementKey(args => args.ElementIndex.ToString()); + }); + }); + + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 2); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.MissedElementInFirstObject && d.MemberPath == "[3]" && d.Value1 == "" && d.Value2 == "4")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "Length" && d.Value1 == "3" && d.Value2 == "4")); + } + + [Test] + public void CompareObjectListByKey() + { + var a1 = new A { ListOfB = new List { new B { Id = 1, Property1 = "Value 1" }, new B { Id = 2, Property1 = "Value 2" } } }; + var a2 = new A { ListOfB = new List { new B { Id = 2, Property1 = "Value two" }, new B { Id = 1, Property1 = "Value one" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey: true); + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 2); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[1].Property1" && d.Value1 == "Value 1" && d.Value2 == "Value one")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[2].Property1" && d.Value1 == "Value 2" && d.Value2 == "Value two")); + } + + [Test] + public void CompareObjectListByCustomKey() + { + var a1 = new A { ListOfB = new List { new B { Id = 1, Property1 = "Value 1", Property2 = "Key1" }, new B { Id = 2, Property1 = "Value 2", Property2 = "Key2" } } }; + var a2 = new A { ListOfB = new List { new B { Id = 1, Property1 = "Value two", Property2 = "Key2" }, new B { Id = 2, Property1 = "Value one", Property2 = "Key1" } } }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(options => + { + options.CompareElementsByKey(keyOptions => + { + keyOptions.UseKey(nameof(B.Property2)); + }); + }); + + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 4); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[Key1].Property1" && d.Value1 == "Value 1" && d.Value2 == "Value one")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[Key1].Id.Value" && d.Value1 == "1" && d.Value2 == "2")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[Key2].Property1" && d.Value1 == "Value 2" && d.Value2 == "Value two")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[Key2].Id.Value" && d.Value1 == "2" && d.Value2 == "1")); + } + + [Test] + public void CompareIntArrayFirstByIndexSecondByKey() + { + var a1 = new A { IntArray = new int[] { 3, 2, 1 }, IntArray2 = new int[] { 3, 2, 1 } }; + var a2 = new A { IntArray = new int[] { 1, 2, 3, 4 }, IntArray2 = new int[] { 1, 2, 3, 4 } }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison((currentProperty, listOptions) => + { + listOptions.CompareUnequalLists(true); + + if (currentProperty.Member.Name == nameof(A.IntArray2)) + { + listOptions.CompareElementsByKey(); + } + }); + + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 6); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "IntArray[0]" && d.Value1 == "3" && d.Value2 == "1")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "IntArray[2]" && d.Value1 == "1" && d.Value2 == "3")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.MissedElementInFirstObject && d.MemberPath == "IntArray[3]" && d.Value1 == "" && d.Value2 == "4")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "IntArray.Length" && d.Value1 == "3" && d.Value2 == "4")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.MissedElementInFirstObject && d.MemberPath == "IntArray2[4]" && d.Value1 == "" && d.Value2 == "4")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "IntArray2.Length" && d.Value1 == "3" && d.Value2 == "4")); + } + + [Test] + public void CompareObjectListFirstByDefaultKeySecondByCustomKey() + { + var a1 = new A + { + ListOfB = new List { new B { Id = 1, Property1 = "Value 1" }, new B { Id = 2, Property1 = "Value 2" } }, + ListOfC = new List { new C { Key = "Key1", Property1 = "Value 3" }, new C { Key = "Key2", Property1 = "Value 4" } } + }; + + var a2 = new A + { + ListOfB = new List { new B { Id = 2, Property1 = "Value two" }, new B { Id = 1, Property1 = "Value one" } } , + ListOfC = new List { new C { Key = "Key2", Property1 = "Value four" }, new C { Key = "Key1", Property1 = "Value three" } } + }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison((currentProperty, listOptions) => + { + listOptions.CompareElementsByKey(); + + if (currentProperty.Member.Name == nameof(A.ListOfC)) + { + listOptions.CompareElementsByKey(keyOptions => keyOptions.UseKey(args => ((C)args.Element).Key)); + } + }); + + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 4); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[1].Property1" && d.Value1 == "Value 1" && d.Value2 == "Value one")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[2].Property1" && d.Value1 == "Value 2" && d.Value2 == "Value two")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfC[Key1].Property1" && d.Value1 == "Value 3" && d.Value2 == "Value three")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfC[Key2].Property1" && d.Value1 == "Value 4" && d.Value2 == "Value four")); + } + + [Test] + public void CompareObjectListFirstByDefaultKeySecondByCustomKeyFormatCustomKey() + { + var a1 = new A + { + ListOfB = new List { new B { Id = 1, Property1 = "Value 1" }, new B { Id = 2, Property1 = "Value 2" } }, + ListOfC = new List { new C { Key = "Key1", Property1 = "Value 3" }, new C { Key = "Key2", Property1 = "Value 4" } } + }; + + var a2 = new A + { + ListOfB = new List { new B { Id = 2, Property1 = "Value two" }, new B { Id = 1, Property1 = "Value one" } }, + ListOfC = new List { new C { Key = "Key2", Property1 = "Value four" }, new C { Key = "Key1", Property1 = "Value three" } } + }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison((currentProperty, listOptions) => + { + listOptions.CompareElementsByKey(); + + if (currentProperty.Member.Name == nameof(A.ListOfC)) + { + listOptions.CompareElementsByKey(keyOptions => + keyOptions + .UseKey(args => new { ((C)args.Element).Key }) + .FormatElementKey(args => $"Key={((C)args.Element).Key}")); + } + }); + + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 4); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[1].Property1" && d.Value1 == "Value 1" && d.Value2 == "Value one")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfB[2].Property1" && d.Value1 == "Value 2" && d.Value2 == "Value two")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfC[Key=Key1].Property1" && d.Value1 == "Value 3" && d.Value2 == "Value three")); + Assert.IsTrue(differences.Any(d => d.DifferenceType == DifferenceTypes.ValueMismatch && d.MemberPath == "ListOfC[Key=Key2].Property1" && d.Value1 == "Value 4" && d.Value2 == "Value four")); + } + + [Test] + public void TestCompareEntireObject() + { + var a1 = new TestClass { IntProperty = 10, StringProperty1 = "a", StringProperty2 = "c", ClassBProperty = new TestClassB { IntPropertyB = 30 } }; + var a2 = new TestClass { IntProperty = 10, StringProperty1 = "b", StringProperty2 = "c", ClassBProperty = new TestClassB { IntPropertyB = 40 } }; + + var comparer = new Comparer(); + //var comparer = new Comparer(); + bool compareResult = comparer.Compare(a1, a2); //Only IntProperty. + //bool calculateAnyDifferencesResult = comparer.CalculateDifferences(a1, a2).Any(); //Only IntProperty. + //var diffsArray = comparer.CalculateDifferences(a1, a2).ToArray(); //All properties. + + /* + IntProperty 10. + IntProperty 20. + */ + } + + public class TestClassB + { + int _intPropertyB; + + public int IntPropertyB + { + get + { + Console.WriteLine($"C IntPropertyB {_intPropertyB}."); + Debug.WriteLine($"IntPropertyB {_intPropertyB}."); + return _intPropertyB; + } + + set => _intPropertyB = value; + } + } + + public class TestClass + { + int _intProperty; + string _stringProperty1; + string _stringProperty2; + TestClassB _classBProperty; + + public int IntProperty + { + get + { + Console.WriteLine($"C IntProperty {_intProperty}."); + Debug.WriteLine($"IntProperty {_intProperty}."); + return _intProperty; + } + + set => _intProperty = value; + } + + public string StringProperty1 + { + get + { + Console.WriteLine($"C StringProperty1 {_stringProperty1}."); + Debug.WriteLine($"StringProperty1 {_stringProperty1}."); + return _stringProperty1; + } + + set => _stringProperty1 = value; + } + + public string StringProperty2 + { + get + { + Console.WriteLine($"C StringProperty2 {_stringProperty2}."); + Debug.WriteLine($"StringProperty2 {_stringProperty2}."); + return _stringProperty2; + } + + set => _stringProperty2 = value; + } + + public TestClassB ClassBProperty + { + get + { + Debug.WriteLine($"ClassBProperty {_classBProperty}."); + return _classBProperty; + } + + set => _classBProperty = value; + } + } + + [Test] + public void CompareObjectListsByComplexKey() + { + var a1 = new A + { + ListOfC = new List + { + new C { Property1 = "Key1a", Property2 ="Key1b", Property3 = "Value 1" }, + new C { Property1 = "Key2a", Property2 = "Key2b", Property3 = "Value 2" } + } + }; + + var a2 = new A + { + ListOfC = new List + { + new C { Property1 = "Key2a", Property2 = "Key2b", Property3 = "Value two" }, + new C { Property1 = "Key1a", Property2 ="Key1b", Property3 = "Value 1" }, + } + }; + + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(listOptions => + { + listOptions.CompareElementsByKey(keyOptions => keyOptions.UseKey(args => new + { + ((C)args.Element).Property1, + ((C)args.Element).Property2 + })); + }); + + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Count() == 1); + + Assert.IsTrue(differences.Any(d => + d.DifferenceType == DifferenceTypes.ValueMismatch + && d.MemberPath == "ListOfC[{ Property1 = Key2a, Property2 = Key2b }].Property3" + && d.Value1 == "Value 2" + && d.Value2 == "Value two")); + } } } diff --git a/ObjectsComparer/ObjectsComparer.Tests/Comparer_MultidimensionalArraysTests.cs b/ObjectsComparer/ObjectsComparer.Tests/Comparer_MultidimensionalArraysTests.cs index b498c3f..e553d8a 100644 --- a/ObjectsComparer/ObjectsComparer.Tests/Comparer_MultidimensionalArraysTests.cs +++ b/ObjectsComparer/ObjectsComparer.Tests/Comparer_MultidimensionalArraysTests.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System; +using System.Linq; using NUnit.Framework; +using ObjectsComparer.Exceptions; using ObjectsComparer.Tests.TestClasses; namespace ObjectsComparer.Tests @@ -13,7 +15,6 @@ public void IntOfIntInequality1() var a1 = new MultidimensionalArrays { IntOfInt = new[] { new[] { 1, 2 } } }; var a2 = new MultidimensionalArrays { IntOfInt = new[] { new[] { 1, 3 } } }; var comparer = new Comparer(); - var isEqual = comparer.Compare(a1, a2, out var differencesEnum); var differences = differencesEnum.ToList(); @@ -25,6 +26,24 @@ public void IntOfIntInequality1() Assert.AreEqual("3", differences[0].Value2); } + [Test] + public void IntOfIntInequality1_CompareByKey() + { + var a1 = new MultidimensionalArrays { IntOfInt = new[] { new[] { 1, 2 } } }; + var a2 = new MultidimensionalArrays { IntOfInt = new[] { new[] { 1, 3 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(options => options.CompareElementsByKey()); + + var comparer = new Comparer(settings); + + Assert.Throws(() => + { + var isEqual = comparer.Compare(a1, a2, out var differencesEnum); + var differences = differencesEnum.ToList(); + }); + } + [Test] public void IntOfIntInequality2() { @@ -64,6 +83,41 @@ public void IntOfIntInequality3() Assert.AreEqual("2", differences[0].Value2); } + [Test] + public void IntOfIntInequality3_CompareUnequalLists() + { + var a1 = new MultidimensionalArrays { IntOfInt = new[] { new[] { 1, 2 } } }; + var a2 = new MultidimensionalArrays { IntOfInt = new[] { new[] { 2, 2 }, new[] { 3, 5 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true)); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(a1, a2, out var differencesEnum); + var differences = differencesEnum.ToList(); + + Assert.IsFalse(isEqual); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(3, differences.Count); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[0].DifferenceType); + Assert.AreEqual("IntOfInt[0][0]", differences[0].MemberPath); + Assert.AreEqual("1", differences[0].Value1); + Assert.AreEqual("2", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("IntOfInt[1]", differences[1].MemberPath); + Assert.AreEqual("", differences[1].Value1); + Assert.AreEqual("System.Int32[]", differences[1].Value2); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[2].DifferenceType); + Assert.AreEqual("IntOfInt.Length", differences[2].MemberPath); + Assert.AreEqual("1", differences[2].Value1); + Assert.AreEqual("2", differences[2].Value2); + } + [Test] public void IntOfIntInequality4() { @@ -100,6 +154,35 @@ public void IntOfIntInequality5() Assert.AreEqual("3", differences[0].Value2); } + [Test] + public void IntOfIntInequality5____() + { + var a1 = new MultidimensionalArrays { IntOfInt = new[] { new[] { 1, 2 }, new[] { 3, 5 } } }; + var a2 = new MultidimensionalArrays { IntOfInt = new[] { new[] { 1, 2 }, new[] { 3, 5, 6 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true)); + + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(a1, a2, out var differencesEnum); + var differences = differencesEnum.ToList(); + + Assert.IsFalse(isEqual); + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[0].DifferenceType); + Assert.AreEqual("IntOfInt[1][2]", differences[0].MemberPath); + Assert.AreEqual("", differences[0].Value1); + Assert.AreEqual("6", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[1].DifferenceType); + Assert.AreEqual("IntOfInt[1].Length", differences[1].MemberPath); + Assert.AreEqual("2", differences[1].Value1); + Assert.AreEqual("3", differences[1].Value2); + } + [Test] public void IntOfIntInequality6() { @@ -322,6 +405,23 @@ public void IntIntInequality7() Assert.AreEqual(string.Empty, differences[0].Value2); } + [Test] + public void IntIntInequality7_CheckDifferenceTreeNode() + { + var a1 = new MultidimensionalArrays { IntInt = new int[0, 0] }; + var a2 = new MultidimensionalArrays { IntInt = null }; + var comparer = new Comparer(); + + var rootNode = comparer.CalculateDifferenceTree(a1, a2); + var differences = rootNode.GetDifferences(true).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual(1, differences.Count()); + Assert.AreEqual("IntInt", differences[0].MemberPath); + Assert.AreEqual(typeof(int[,]).FullName, differences[0].Value1); + Assert.AreEqual(string.Empty, differences[0].Value2); + } + [Test] public void IntIntEquality1() { diff --git a/ObjectsComparer/ObjectsComparer.Tests/Comparer_NonGenericEnumerableTests.cs b/ObjectsComparer/ObjectsComparer.Tests/Comparer_NonGenericEnumerableTests.cs index debcb82..3d8ed70 100644 --- a/ObjectsComparer/ObjectsComparer.Tests/Comparer_NonGenericEnumerableTests.cs +++ b/ObjectsComparer/ObjectsComparer.Tests/Comparer_NonGenericEnumerableTests.cs @@ -1,7 +1,15 @@ -using System.Collections; +using System; +using System.Collections; +using System.Diagnostics; using System.Linq; +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; using NUnit.Framework; using ObjectsComparer.Tests.TestClasses; +using ObjectsComparer.Tests.Utils; +using ObjectsComparer.Utils; namespace ObjectsComparer.Tests { @@ -20,6 +28,39 @@ public void Equality() Assert.IsTrue(isEqual); } + [Test] + public void Equality_CompareByKey() + { + var a1 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str2" }, new B { Property1 = "Str1" } } }; + var a2 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareElementsByKey(keyOptions => keyOptions.UseKey("Property1"))); + var comparer = new Comparer(settings); + + var isEqual = comparer.Compare(a1, a2, out _); + + Assert.IsTrue(isEqual); + } + + [Test] + [TestCase(true, 0)] + [TestCase(false, 4)] + public void ShortcutConfigureListComparison(bool compareElementsByKey, int expectedDiffsCount) + { + var a1 = new int?[] { 1, 2, 3, null }; + var a2 = new int?[] { null, 3, 2, 1 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.IsTrue(differences.Count == expectedDiffsCount); + } + [Test] public void InequalityCount() { @@ -36,6 +77,90 @@ public void InequalityCount() Assert.AreEqual("1", differences.First().Value2); } + [Test] + public void InequalityCount_CompareUnequalLists() + { + var a1 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + var a2 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str1" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true)); + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual("NonGenericEnumerable", differences.First().MemberPath); + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + Assert.AreEqual("2", differences.First().Value1); + Assert.AreEqual("1", differences.First().Value2); + + var diff2 = differences[1]; + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, diff2.DifferenceType); + Assert.AreEqual("NonGenericEnumerable[1]", diff2.MemberPath); + Assert.AreNotEqual(string.Empty, diff2.Value1); + Assert.AreEqual(string.Empty, diff2.Value2); + } + + [Test] + public void InequalityCount_CompareUnequalLists_CompareByKey() + { + var a1 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + var a2 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str1" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => + { + listOptions.CompareUnequalLists(true); + listOptions.CompareElementsByKey(keyOptions => + { + keyOptions.UseKey("Property1"); + keyOptions.FormatElementKey(args => $"Property1={args.ElementKey}"); + }); + }); + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual("NonGenericEnumerable", differences.First().MemberPath); + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + Assert.AreEqual("2", differences.First().Value1); + Assert.AreEqual("1", differences.First().Value2); + + var diff2 = differences[1]; + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, diff2.DifferenceType); + Assert.AreEqual("NonGenericEnumerable[Property1=Str2]", diff2.MemberPath); + Assert.AreNotEqual(string.Empty, diff2.Value1); + Assert.AreEqual(string.Empty, diff2.Value2); + } + + [Test] + public void InequalityCount_CompareUnequalLists_CompareByKey_DontFormatKey() + { + var a1 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str1" }, new B { Property1 = "Str2" } } }; + var a2 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str1" } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true).CompareElementsByKey(keyOptions => keyOptions.UseKey("Property1"))); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + CollectionAssert.IsNotEmpty(differences); + Assert.AreEqual("NonGenericEnumerable", differences.First().MemberPath); + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences.First().DifferenceType); + Assert.AreEqual("2", differences.First().Value1); + Assert.AreEqual("1", differences.First().Value2); + + var diff2 = differences[1]; + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, diff2.DifferenceType); + Assert.AreEqual("NonGenericEnumerable[Str2]", diff2.MemberPath); + Assert.AreNotEqual(string.Empty, diff2.Value1); + Assert.AreEqual(string.Empty, diff2.Value2); + } + [Test] public void InequalityProperty() { @@ -51,6 +176,27 @@ public void InequalityProperty() Assert.AreEqual("Str3", differences.First().Value2); } + [Test] + public void InequalityProperty_CompareByKey() + { + var a1 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str1", Id = 1 }, new B { Property1 = "Str2", Id = 2 } } }; + var a2 = new A { NonGenericEnumerable = new ArrayList { new B { Property1 = "Str3", Id = 2 }, new B { Property1 = "Str1", Id = 1 } } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey: true); + + var comparer = new Comparer(settings); + + var rootNode = comparer.CalculateDifferenceTree(a1, a2); + var differences = rootNode.GetDifferences(true); + + CollectionAssert.IsNotEmpty(differences); + var diff = differences.First(); + Assert.AreEqual("NonGenericEnumerable[2].Property1", diff.MemberPath); + Assert.AreEqual("Str2", diff.Value1); + Assert.AreEqual("Str3", diff.Value2); + } + [Test] public void NullElementsEquality() { @@ -63,11 +209,54 @@ public void NullElementsEquality() Assert.IsTrue(isEqual); } + [Test] + public void NullElementsEquality_CompareUnequalLists() + { + var a1 = new A { NonGenericEnumerable = new ArrayList { null } }; + var a2 = new A { NonGenericEnumerable = new ArrayList { null, null } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true)); + + var comparer = new Comparer(settings); + var isEqual = comparer.Compare(a1, a2, out var diffs); + var differences = diffs.ToArray(); + Assert.IsTrue(differences.Count() == 2); + Assert.IsTrue(differences[0].DifferenceType == DifferenceTypes.NumberOfElementsMismatch); + Assert.IsTrue(differences[0].Value1 == "1"); + Assert.IsTrue(differences[0].Value2 == "2"); + Assert.IsTrue(differences[1].DifferenceType == DifferenceTypes.MissedElementInFirstObject); + Assert.IsTrue(differences[1].MemberPath == "NonGenericEnumerable[1]"); + Assert.IsTrue(differences[1].Value1 == string.Empty); + Assert.IsTrue(differences[1].Value2 == string.Empty); + Assert.IsFalse(isEqual); + } + + [Test] + public void NullElementsEquality_CompareUnequalLists_CompareByKey() + { + var a1 = new A { NonGenericEnumerable = new ArrayList { null } }; + var a2 = new A { NonGenericEnumerable = new ArrayList { null, null } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true).CompareElementsByKey()); + + var comparer = new Comparer(settings); + var isEqual = comparer.Compare(a1, a2, out var diffs); + var differences = diffs.ToArray(); + Assert.IsTrue(differences.Count() == 1); + Assert.IsTrue(differences[0].DifferenceType == DifferenceTypes.NumberOfElementsMismatch); + Assert.IsTrue(differences[0].Value1 == "1"); + Assert.IsTrue(differences[0].Value2 == "2"); + Assert.IsFalse(isEqual); + } + [Test] public void NullAndNotNullElementsInequality() { var a1 = new A { NonGenericEnumerable = new ArrayList { null, "Str1" } }; var a2 = new A { NonGenericEnumerable = new ArrayList { "Str2", null } }; + var comparer = new Comparer(); var differences = comparer.CalculateDifferences(a1, a2).ToList(); @@ -81,6 +270,32 @@ public void NullAndNotNullElementsInequality() Assert.AreEqual(string.Empty, differences[1].Value2); } + [Test] + public void NullAndNotNullElementsInequality_CompareByKey() + { + var a1 = new A { NonGenericEnumerable = new ArrayList { null, "Str1" } }; + var a2 = new A { NonGenericEnumerable = new ArrayList { "Str2", null } }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareElementsByKey: true); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.MissedElementInSecondObject, differences[0].DifferenceType); + Assert.AreEqual("NonGenericEnumerable[Str1]", differences[0].MemberPath); + Assert.AreEqual("Str1", differences[0].Value1); + Assert.AreEqual(string.Empty, differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("NonGenericEnumerable[Str2]", differences[1].MemberPath); + Assert.AreEqual(string.Empty, differences[1].Value1); + Assert.AreEqual("Str2", differences[1].Value2); + } + [Test] public void InequalityType() { diff --git a/ObjectsComparer/ObjectsComparer.Tests/ComparisonSettingsTests.cs b/ObjectsComparer/ObjectsComparer.Tests/ComparisonSettingsTests.cs index e5460d1..1cc55a7 100644 --- a/ObjectsComparer/ObjectsComparer.Tests/ComparisonSettingsTests.cs +++ b/ObjectsComparer/ObjectsComparer.Tests/ComparisonSettingsTests.cs @@ -1,5 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections; +using System.Linq; +using System.Collections.Generic; using NUnit.Framework; +using ObjectsComparer.Tests.TestClasses; +using ObjectsComparer.Tests.Utils; +using System.Reflection; namespace ObjectsComparer.Tests { @@ -48,5 +54,284 @@ public void WronkType() Assert.Throws(() => settings.GetCustomSetting("setting1")); } + + /// + /// Whether list comparison by key is correctly set. + /// + [Test] + public void CompareListElementsByKeyIsCorrectlySet() + { + //Client side. + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison((curentNode, listOptions) => + { + listOptions.CompareUnequalLists(true); + + listOptions.CompareElementsByKey(keyOptions => + { + keyOptions.UseKey("Key"); + keyOptions.ThrowKeyNotFound(false); + }); + + var currentMember = curentNode.Member; + }); + + //Component side. + var listComparisonOptions = ListComparisonOptions.Default(); + var currentNode = new DifferenceTreeNode(); + settings.ListComparisonOptionsAction(currentNode, listComparisonOptions); + var listElementComparisonByKeyOptions = ListElementComparisonByKeyOptions.Default(); + listComparisonOptions.KeyOptionsAction(listElementComparisonByKeyOptions); + + Assert.AreEqual(true, listComparisonOptions.UnequalListsComparisonEnabled); + Assert.AreEqual(true, listComparisonOptions.ElementSearchMode == ListElementSearchMode.Key); + Assert.AreEqual(false, listElementComparisonByKeyOptions.ThrowKeyNotFoundEnabled); + } + + /// + /// Whether backward compatibility is ensured i.e. sequential comparing of equal lists. + /// + [Test] + public void ListComparisonConfigurationBackwardCompatibilityEnsured() + { + var options = ListComparisonOptions.Default(); + + Assert.AreEqual(false, options.UnequalListsComparisonEnabled); + Assert.AreEqual(true, options.ElementSearchMode == ListElementSearchMode.Index); + } + + [Test] + public void FluentTest_CompareUnequalLists() + { + var a1 = new int[] { 3, 2, 1 }; + var a2 = new int[] { 1, 2, 3, 4 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareUnequalLists: true); + + var comparer = new Comparer(settings); + var rootNode = comparer.CalculateDifferenceTree(a1.GetType(), a1, a2); + var differences = rootNode.GetDifferences(true).ToList(); + + Assert.AreEqual(4, differences.Count); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[0].DifferenceType); + Assert.AreEqual("[0]", differences[0].MemberPath); + Assert.AreEqual("3", differences[0].Value1); + Assert.AreEqual("1", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[1].DifferenceType); + Assert.AreEqual("[2]", differences[1].MemberPath); + Assert.AreEqual("1", differences[1].Value1); + Assert.AreEqual("3", differences[1].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[2].DifferenceType); + Assert.AreEqual("[3]", differences[2].MemberPath); + Assert.AreEqual("", differences[2].Value1); + Assert.AreEqual("4", differences[2].Value2); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[3].DifferenceType); + Assert.AreEqual("Length", differences[3].MemberPath); + Assert.AreEqual("3", differences[3].Value1); + Assert.AreEqual("4", differences[3].Value2); + } + + [Test] + public void FluentTest_CompareUnequalLists_ByKey() + { + var a1 = new int[] { 3, 2, 1 }; + var a2 = new int[] { 1, 2, 3, 4 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareUnequalLists: true, compareElementsByKey: true); + + var comparer = new Comparer(settings); + + var rootNode = comparer.CalculateDifferenceTree(a1.GetType(), a1, a2); + var differences = rootNode.GetDifferences(true).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[0].DifferenceType); + Assert.AreEqual("[4]", differences[0].MemberPath); + Assert.AreEqual("", differences[0].Value1); + Assert.AreEqual("4", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[1].DifferenceType); + Assert.AreEqual("Length", differences[1].MemberPath); + Assert.AreEqual("3", differences[1].Value1); + Assert.AreEqual("4", differences[1].Value2); + } + + [Test] + public void FluentTest_CompareUnequalLists_ByKey_FormatKey() + { + var a1 = new int[] { 3, 2, 1 }; + var a2 = new int[] { 1, 2, 3, 4 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions + .CompareUnequalLists(true) + .CompareElementsByKey(keyOptions => keyOptions.FormatElementKey(args => $"Key={args.ElementKey}"))); + + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[0].DifferenceType); + Assert.AreEqual("[Key=4]", differences[0].MemberPath); + Assert.AreEqual("", differences[0].Value1); + Assert.AreEqual("4", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[1].DifferenceType); + Assert.AreEqual("Length", differences[1].MemberPath); + Assert.AreEqual("3", differences[1].Value1); + Assert.AreEqual("4", differences[1].Value2); + } + + [Test] + public void FluentTest_CompareUnequalLists_CompareElementsByKey_FormatKey_DefaultNullElementIdentifier() + { + var a1 = new int?[] { 3, 2, 1 }; + var a2 = new int?[] { 1, 2, 3, 4, null }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions + .CompareUnequalLists(true) + .CompareElementsByKey(keyOptions => keyOptions.FormatElementKey(args => $"Key={args.ElementKey}"))); + + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(3, differences.Count); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[0].DifferenceType); + Assert.AreEqual("[Key=4]", differences[0].MemberPath); + Assert.AreEqual("", differences[0].Value1); + Assert.AreEqual("4", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("[NullAtIdx=4]", differences[1].MemberPath); + Assert.AreEqual("", differences[1].Value1); + Assert.AreEqual("", differences[1].Value2); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[2].DifferenceType); + Assert.AreEqual("Length", differences[2].MemberPath); + Assert.AreEqual("3", differences[2].Value1); + Assert.AreEqual("5", differences[2].Value2); + } + + [Test] + public void FluentTest_CompareUnequalLists_CompareElementsByKey_FormatKey_FormatNullElementIdentifier() + { + var a1 = new int?[] { 3, 2, 1 }; + var a2 = new int?[] { 1, 2, 3, 4, null }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions + .CompareUnequalLists(true) + .CompareElementsByKey(keyOptions => keyOptions + .FormatElementKey(formatArgs => $"Key={formatArgs.ElementKey}") + .FormatNullElementIdentifier(formatArgs => $"Null at {formatArgs.ElementIndex}"))); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(3, differences.Count); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[0].DifferenceType); + Assert.AreEqual("[Key=4]", differences[0].MemberPath); + Assert.AreEqual("", differences[0].Value1); + Assert.AreEqual("4", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("[Null at 4]", differences[1].MemberPath); + Assert.AreEqual("", differences[1].Value1); + Assert.AreEqual("", differences[1].Value2); + + Assert.AreEqual(DifferenceTypes.ValueMismatch, differences[2].DifferenceType); + Assert.AreEqual("Length", differences[2].MemberPath); + Assert.AreEqual("3", differences[2].Value1); + Assert.AreEqual("5", differences[2].Value2); + } + + [Test] + public void FluentTest_List() + { + var a1 = new List { 3, 2, 1 }; + var a2 = new List { 1, 2, 3, 4 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(listOptions => listOptions.CompareUnequalLists(true).CompareElementsByKey()); + + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(a1, a2).ToList(); + + Assert.AreEqual(2, differences.Count); + + Assert.AreEqual(DifferenceTypes.NumberOfElementsMismatch, differences[0].DifferenceType); + Assert.AreEqual("", differences[0].MemberPath); + Assert.AreEqual("3", differences[0].Value1); + Assert.AreEqual("4", differences[0].Value2); + + Assert.AreEqual(DifferenceTypes.MissedElementInFirstObject, differences[1].DifferenceType); + Assert.AreEqual("[4]", differences[1].MemberPath); + Assert.AreEqual("", differences[1].Value1); + Assert.AreEqual("4", differences[1].Value2); + } + + [Test] + public static void LambdaTest() + { + var game = new VariableCaptureGame(); + + int gameInput = 5; + game.Run(gameInput); + + int jTry = 10; + bool result = game.isEqualToCapturedLocalVariable(jTry); + Console.WriteLine($"Captured local variable is equal to {jTry}: {result}"); + + int anotherJ = 3; + game.updateCapturedLocalVariable(anotherJ); + + bool equalToAnother = game.isEqualToCapturedLocalVariable(anotherJ); + Console.WriteLine($"Another lambda observes a new value of captured variable: {equalToAnother}"); + } + // Output: + // Local variable before lambda invocation: 0 + // 10 is greater than 5: True + // Local variable after lambda invocation: 10 + // Captured local variable is equal to 10: True + // 3 is greater than 5: False + // Another lambda observes a new value of captured variable: True + } + + public class VariableCaptureGame + { + internal Action updateCapturedLocalVariable; + internal Func isEqualToCapturedLocalVariable; + + + public void Run(int input) + { + int j = 0; + + updateCapturedLocalVariable = x => + { + j = x; + bool result = j > input; + Console.WriteLine($"{j} is greater than {input}: {result}"); + }; + + isEqualToCapturedLocalVariable = x => x == j; + + Console.WriteLine($"Local variable before lambda invocation: {j}"); + updateCapturedLocalVariable(10); + Console.WriteLine($"Local variable after lambda invocation: {j}"); + } } } diff --git a/ObjectsComparer/ObjectsComparer.Tests/DifferenceConfigurationTests.cs b/ObjectsComparer/ObjectsComparer.Tests/DifferenceConfigurationTests.cs new file mode 100644 index 0000000..7df0d1c --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Tests/DifferenceConfigurationTests.cs @@ -0,0 +1,656 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using NSubstitute; +using NUnit.Framework; +using ObjectsComparer.Tests.TestClasses; +using System.Reflection; +using System.IO; +using System.Text; +using System.ComponentModel; +using System.Diagnostics; +using DescriptionAttribute = System.ComponentModel.DescriptionAttribute; +using System.Collections; + +namespace ObjectsComparer.Tests + +{ + [TestFixture] + public class DifferenceConfigurationTests + { + [Test] + public void PreserveRawValuesDefaultBehavior() + { + var a1 = new A() { IntProperty = 10 }; + var a2 = new A() { IntProperty = 11 }; + + var comparer = new Comparer(); + + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences[0].RawValue1 == null); + Assert.IsTrue(differences[0].RawValue2 == null); + } + + [Test] + public void PreserveRawValues() + { + var a1 = new A() { IntProperty = 10 }; + var a2 = new A() { IntProperty = 11 }; + + var settings = new ComparisonSettings(); + settings.ConfigureDifference(includeRawValues: true); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue((int)differences[0].RawValue1 == 10); + Assert.IsTrue((int)differences[0].RawValue2 == 11); + } + + [Test] + public void DontPreserveRawValues() + { + var a1 = new A() { IntProperty = 10 }; + var a2 = new A() { IntProperty = 11 }; + + var settings = new ComparisonSettings(); + settings.ConfigureDifference(includeRawValues: false); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences[0].RawValue1 == null); + Assert.IsTrue(differences[0].RawValue2 == null); + } + + [Test] + public void PreserveRawValuesConditionally() + { + var a1 = new A() { IntProperty = 10, TestProperty1 = "TestProperty1value" }; + var a2 = new A() { IntProperty = 11, TestProperty1 = "TestProperty2value" }; + + var settings = new ComparisonSettings(); + + settings.ConfigureDifference((currentProperty, options) => + { + options.IncludeRawValues(currentProperty.Member.Name == "IntProperty"); + }); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences[0].MemberPath == "IntProperty"); + Assert.IsTrue((int)differences[0].RawValue1 == 10); + Assert.IsTrue((int)differences[0].RawValue2 == 11); + + Assert.IsTrue(differences[1].MemberPath == "TestProperty1"); + Assert.IsTrue(differences[1].RawValue1 == null); + Assert.IsTrue(differences[1].RawValue2 == null); + } + + [Test] + public void CustomizeMemberPath() + { + var a1 = new A() { ClassB = new B { Property1 = "value1 " }, IntProperty = 10, TestProperty2 = "value1", ListOfC = new List { new C { Property1 = "property1" } } }; + var a2 = new A() { ClassB = new B { Property1 = "value2 " }, IntProperty = 11, TestProperty2 = "value2", ListOfC = new List { new C { Property1 = "property2" } } }; + + var settings = new ComparisonSettings(); + + settings + .ConfigureDifference((currentProperty, options) => + { + if (currentProperty.Member.Name == nameof(A.IntProperty)) + { + options.UseDifferenceFactory(args => new Difference("Integer property", args.DefaultDifference.Value1, args.DefaultDifference.Value2)); + } + else if (currentProperty.Member.Name == nameof(C.Property1)) + { + if (currentProperty.Ancestor.Member.Name == "ClassB") + { + options.UseDifferenceFactory(args => new Difference("First property of Class B", args.DefaultDifference.Value1, args.DefaultDifference.Value2)); + } + else + { + options.UseDifferenceFactory(args => new Difference("First property of class A", args.DefaultDifference.Value1, args.DefaultDifference.Value2)); + } + } + }) + .ConfigureDifferencePath((parentProperty, options) => + { + if (parentProperty.Member.Name == nameof(A.ListOfC)) + { + options.UseInsertPathFactory(args => "Collection of C objects"); + } + }); + + var comparer = new Comparer(settings); + + var differences = comparer.CalculateDifferences(a1, a2).ToArray(); + + Assert.IsTrue(differences.Any(diff => diff.MemberPath == "Integer property")); + Assert.IsTrue(differences.Any(diff => diff.MemberPath == "TestProperty2")); + Assert.IsTrue(differences.Any(diff => diff.MemberPath == "Collection of C objects[0].First property of class A")); + Assert.IsTrue(differences.Any(diff => diff.MemberPath == "ClassB.First property of Class B")); + } + + [Test] + public void CalculateCompletedDifferenceTree() + { + var student1 = new Student + { + Person = new Person + { + FirstName = "Daniel", + + ListOfAddress1 = new List
+ { + new Address { City = "Prague", Country = "Czech republic" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var student2 = new Student + { + Person = new Person + { + FirstName = "Dan", + + ListOfAddress1 = new List
+ { + new Address { City = "Olomouc", Country = "Czechia" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var comparer = new Comparer(); + + var rootNode = comparer.CalculateDifferenceTree(student1, student2); + + Assert.AreEqual(3, rootNode.GetDifferences(recursive: true).Count()); + + var stringBuilder = new StringBuilder(); + WriteDifferenceTree(rootNode, 0, stringBuilder); + var differenceTreeStr = stringBuilder.ToString(); + + /* + * differenceTreeStr: + ? + Person + FirstName + Difference: DifferenceType=ValueMismatch, MemberPath='Person.FirstName', Value1='Daniel', Value2='Dan'. + LastName + Birthdate + PhoneNumber + ListOfAddress1 + [0] + Id + City + Difference: DifferenceType=ValueMismatch, MemberPath='Person.ListOfAddress1[0].City', Value1='Prague', Value2='Olomouc'. + Country + Difference: DifferenceType=ValueMismatch, MemberPath='Person.ListOfAddress1[0].Country', Value1='Czech republic', Value2='Czechia'. + State + PostalCode + Street + [1] + Id + City + Country + State + PostalCode + Street + ListOfAddress2 + */ + + using (var sr = new StringReader(differenceTreeStr)) + { + var expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "?"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Person"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "FirstName"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Person.FirstName', Value1='Daniel', Value2='Dan'."); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "LastName"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Birthdate"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "PhoneNumber"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "ListOfAddress1"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "[0]"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Id"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "City"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Person.ListOfAddress1[0].City', Value1='Prague', Value2='Olomouc'."); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Country"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Person.ListOfAddress1[0].Country', Value1='Czech republic', Value2='Czechia'."); + } + + rootNode.Shrink(); + + stringBuilder = new StringBuilder(); + WriteDifferenceTree(rootNode, 0, stringBuilder); + differenceTreeStr = stringBuilder.ToString(); + + /* differenceTreeStr (shrinked): + ? + Person + FirstName + Difference: DifferenceType=ValueMismatch, MemberPath='Person.FirstName', Value1='Daniel', Value2='Dan'. + ListOfAddress1 + [0] + City + Difference: DifferenceType=ValueMismatch, MemberPath='Person.ListOfAddress1[0].City', Value1='Prague', Value2='Olomouc'. + Country + Difference: DifferenceType=ValueMismatch, MemberPath='Person.ListOfAddress1[0].Country', Value1='Czech republic', Value2='Czechia'. + */ + + using (var sr = new StringReader(differenceTreeStr)) + { + var expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "?"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Person"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "FirstName"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Person.FirstName', Value1='Daniel', Value2='Dan'."); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "ListOfAddress1"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "[0]"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "City"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Person.ListOfAddress1[0].City', Value1='Prague', Value2='Olomouc'."); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Country"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Person.ListOfAddress1[0].Country', Value1='Czech republic', Value2='Czechia'."); + } + } + + [Test] + public void CalculateUncompletedDifferenceTree() + { + var student1 = new Student + { + Person = new Person + { + FirstName = "Daniel", + + ListOfAddress1 = new List
+ { + new Address { City = "Prague", Country = "Czech republic" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var student2 = new Student + { + Person = new Person + { + FirstName = "Dan", + + ListOfAddress1 = new List
+ { + new Address { City = "Olomouc", Country = "Czechia" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var comparer = new Comparer(); + + var rootNode = comparer.CalculateDifferenceTree( + student1, + student2, + currentContext => currentContext.RootNode.GetDifferences(recursive: true).Any() == false); + + Assert.AreEqual(1, rootNode.GetDifferences(recursive: true).Count()); + + rootNode.Shrink(); + + var stringBuilder = new StringBuilder(); + WriteDifferenceTree(rootNode, 0, stringBuilder); + var differenceTreeStr = stringBuilder.ToString(); + + /* differenceTreeStr (shrinked): + ? + Person + FirstName + Difference: DifferenceType=ValueMismatch, MemberPath='Person.FirstName', Value1='Daniel', Value2='Dan'. + */ + + using (var sr = new StringReader(differenceTreeStr)) + { + var expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "?"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Person"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "FirstName"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Person.FirstName', Value1='Daniel', Value2='Dan'."); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine, null); + } + } + + [Test] + public void CalculateDifferencesTranslateMembers() + { + var student1 = new Student + { + Person = new Person + { + ListOfAddress1 = new List
+ { + new Address { City = "Prague", Country = "Czech republic" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var student2 = new Student + { + Person = new Person + { + ListOfAddress1 = new List
+ { + new Address { City = "Olomouc", Country = "Czechia" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var settings = new ComparisonSettings(); + settings.ConfigureDifferences(defaultMember => TranslateToCzech(defaultMember?.Name)); + var comparer = new Comparer(settings); + var differences = comparer.CalculateDifferences(student1, student2).ToArray(); + + Assert.AreEqual(2, differences.Count()); + + Assert.IsTrue(differences.Any(d => + d.DifferenceType == DifferenceTypes.ValueMismatch && + d.MemberPath == "Osoba.Seznam adres 1[0].Město" && + d.Value1 == "Prague" && d.Value2 == "Olomouc")); + + Assert.IsTrue(differences.Any(d => + d.DifferenceType == DifferenceTypes.ValueMismatch && + d.MemberPath == "Osoba.Seznam adres 1[0].Stát" && + d.Value1 == "Czech republic" && d.Value2 == "Czechia")); + } + + [Test] + public void CalculateDifferenceTreeTranslateMembersUsingAttributes() + { + var student1 = new Student + { + Person = new Person + { + ListOfAddress1 = new List
+ { + new Address { City = "Prague", Country = "Czech republic" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var student2 = new Student + { + Person = new Person + { + ListOfAddress1 = new List
+ { + new Address { City = "Olomouc", Country = "Czechia" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var settings = new ComparisonSettings(); + settings.ConfigureDifferences(defaultMember => TranslateToCzech(defaultMember)); + var comparer = new Comparer(settings); + var rootNode = comparer.CalculateDifferenceTree(student1, student2); + rootNode.Shrink(); + + Assert.AreEqual(2, rootNode.GetDifferences(recursive: true).Count()); + + var stringBuilder = new StringBuilder(); + WriteDifferenceTree(rootNode, 0, stringBuilder); + var differenceTreeStr = stringBuilder.ToString(); + + /* differenceTreeStr (shrinked): + ? + Člověk + Kolekce adres + [0] + Aglomerace (město) + Difference: DifferenceType=ValueMismatch, MemberPath='Člověk.Kolekce adres[0].Aglomerace (město)', Value1='Prague', Value2='Olomouc'. + Země (stát) + Difference: DifferenceType=ValueMismatch, MemberPath='Člověk.Kolekce adres[0].Země (stát)', Value1='Czech republic', Value2='Czechia'. + */ + + using (var sr = new StringReader(differenceTreeStr)) + { + var expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "?"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Člověk"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Kolekce adres"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "[0]"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Aglomerace (město)"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Člověk.Kolekce adres[0].Aglomerace (město)', Value1='Prague', Value2='Olomouc'."); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Země (stát)"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Člověk.Kolekce adres[0].Země (stát)', Value1='Czech republic', Value2='Czechia'."); + } + } + + [Test] + public void CalculateDifferenceTreeTranslateMembers() + { + var student1 = new Student + { + Person = new Person + { + ListOfAddress1 = new List
+ { + new Address { City = "Prague", Country = "Czech republic" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var student2 = new Student + { + Person = new Person + { + ListOfAddress1 = new List
+ { + new Address { City = "Olomouc", Country = "Czechia" }, + new Address { City = "Pilsen", Country = "Czech republic" } + } + } + }; + + var settings = new ComparisonSettings(); + settings.ConfigureDifferences(defaultMember => TranslateToCzech(defaultMember?.Name)); + var comparer = new Comparer(settings); + var rootNode = comparer.CalculateDifferenceTree(student1, student2); + rootNode.Shrink(); + + Assert.AreEqual(2, rootNode.GetDifferences(recursive: true).Count()); + + var stringBuilder = new StringBuilder(); + WriteDifferenceTree(rootNode, 0, stringBuilder); + var differenceTreeStr = stringBuilder.ToString(); + + /* differenceTreeStr (shrinked): + ? + Osoba + Seznam adres 1 + [0] + Město + Difference: DifferenceType=ValueMismatch, MemberPath='Osoba.Seznam adres 1[0].Město', Value1='Prague', Value2='Olomouc'. + Stát + Difference: DifferenceType=ValueMismatch, MemberPath='Osoba.Seznam adres 1[0].Stát', Value1='Czech republic', Value2='Czechia'. + */ + + using (var sr = new StringReader(differenceTreeStr)) + { + var expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "?"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Osoba"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Seznam adres 1"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "[0]"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Město"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Osoba.Seznam adres 1[0].Město', Value1='Prague', Value2='Olomouc'."); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Stát"); + expectedLine = sr.ReadLine(); + Assert.AreEqual(expectedLine.Trim(), "Difference: DifferenceType=ValueMismatch, MemberPath='Osoba.Seznam adres 1[0].Stát', Value1='Czech republic', Value2='Czechia'."); + } + } + + string TranslateToCzech(MemberInfo member) + { + if (member == null) + { + return null; + } + + var descriptionAttr = member.GetCustomAttribute(); + + if (descriptionAttr != null) + { + return descriptionAttr.Description; + } + + return TranslateToCzech(member.Name); + } + + string TranslateToCzech(string original) + { + var translated = original; + + switch (original) + { + case "Person": + translated = "Osoba"; + break; + case "FirstName": + translated = "Křestní jméno"; + break; + case "LastName": + translated = "Příjmení"; + break; + case "Birthdate": + translated = "Datum narození"; + break; + case "PhoneNumber": + translated = "Číslo telefonu"; + break; + case "ListOfAddress1": + translated = "Seznam adres 1"; + break; + case "City": + translated = "Město"; + break; + case "Country": + translated = "Stát"; + break; + default: + break; + } + + return translated; + } + + void WriteDifferenceTree(IDifferenceTreeNode node, int level, StringBuilder stringBuilder) + { + var blankMemberName = "?"; + string indent = String.Concat(Enumerable.Repeat(" ", 2 * level)); + + if (TreeNodeIsListItem(node) == false) + { + var memberName = node?.Member?.Name ?? blankMemberName; + var line = indent + memberName; + stringBuilder.AppendLine(line); + Debug.WriteLine(line); + } + + foreach (var diff in node.Differences) + { + var line = indent + String.Concat(Enumerable.Repeat(" ", 2)) + diff.ToString(); + stringBuilder.AppendLine(line); + Debug.WriteLine(line); + } + + level++; + + var descendants = node.Descendants.ToArray(); + + for (int i = 0; i < descendants.Length; i++) + { + var desc = descendants[i]; + + if (TreeNodeIsListItem(desc)) + { + var line = indent + String.Concat(Enumerable.Repeat(" ", 2)) + $"[{GetIndex(desc)}]"; + stringBuilder.AppendLine(line); + Debug.WriteLine(line); + } + + WriteDifferenceTree(desc, level, stringBuilder); + } + } + + int? GetIndex(IDifferenceTreeNode node) + { + var itemx = node.Ancestor.Descendants + .Select((descendant, index) => new { Index = index, Descendant = descendant }).Where(n => n.Descendant == node) + .FirstOrDefault(); + + return itemx?.Index; + } + + bool TreeNodeIsListItem(IDifferenceTreeNode node) + { + if (node.Ancestor?.Member?.Info is PropertyInfo pi && typeof(IEnumerable).IsAssignableFrom(pi.PropertyType) && pi.PropertyType != typeof(string)) + { + return true; + } + + return false; + } + } +} + diff --git a/ObjectsComparer/ObjectsComparer.Tests/DifferenceTreeNodeTests.cs b/ObjectsComparer/ObjectsComparer.Tests/DifferenceTreeNodeTests.cs new file mode 100644 index 0000000..05c61af --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Tests/DifferenceTreeNodeTests.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections; +using System.Linq; +using System.Collections.Generic; +using NUnit.Framework; +using ObjectsComparer.Tests.TestClasses; +using ObjectsComparer.Tests.Utils; +using System.Reflection; +using System.Diagnostics; +using ObjectsComparer.Utils; +using ObjectsComparer.DifferenceTreeExtensions; +using ObjectsComparer.Exceptions; + +namespace ObjectsComparer.Tests +{ + [TestFixture] + internal class DifferenceTreeNodeTests + { + [Test] + public void XXX() + { + var a1 = new A { ClassB = new B { Property1 = "hello" } }; + var a2 = new A { ClassB = new B { Property1 = "hallo" } }; + + var comparer = new Comparer(); + var rootNode = comparer.CalculateDifferenceTree(a1, a2); + var differences = rootNode.GetDifferences().ToList(); + var classB = rootNode.Descendants.Single(n => n.Member.Name == nameof(A.ClassB)); + var property1 = classB.Descendants.Single(n => n.Member.Name == nameof(B.Property1)); + + Assert.AreEqual(1, differences.Count); + Assert.AreEqual(differences[0].MemberPath, $"{classB.Member.Name}.{property1.Member.Name}"); + } + + [Test] + public void DifferenceTreeNodeMember_Member_Correct_MemberName() + { + var treeNodeMember = new DifferenceTreeNodeMember(name: "Property1"); + Assert.AreEqual("Property1", treeNodeMember.Name); + Assert.AreEqual(null, treeNodeMember.Info); + } + + [Test] + public void DifferenceTreeNodeMember_Member_Correct_Member() + { + var memberInfo = typeof(Address).GetMember(nameof(Address.Country)).Single(); + var treeNodeMember = new DifferenceTreeNodeMember(memberInfo, memberInfo.Name); + Assert.AreEqual(nameof(Address.Country), treeNodeMember.Info.Name); + Assert.AreEqual(nameof(Address.Country), treeNodeMember.Name); + } + + [Test] + public void CustomDifferenceTreeNode() + { + var settings = new ComparisonSettings(); + var rootNode = DifferenceTreeNodeProvider.CreateRootNode(); + + settings.ConfigureDifferenceTree((currentNode, options) => + { + options.UseDifferenceTreeNodeFactory(treeNodeMember => new CustomDifferenceTreeNode(treeNodeMember, rootNode)); + }); + + var treeNode = DifferenceTreeNodeProvider.CreateNode(settings, rootNode, "Property1"); + + Assert.AreEqual("Property1", treeNode.Member.Name); + Assert.IsTrue(treeNode.GetType() == typeof(CustomDifferenceTreeNode)); + Assert.IsTrue(treeNode.Ancestor == rootNode); + } + + [Test] + public void CustomDifferenceTreeNodeMember() + { + var settings = new ComparisonSettings(); + var rootNode = DifferenceTreeNodeProvider.CreateRootNode(); + + settings.ConfigureDifferenceTree((currentContex, options) => + { + options.UseDifferenceTreeNodeMemberFactory(defaultMember => new CustomDifferenceTreeNodeMember(defaultMember.Name)); + }); + + var treeNode = DifferenceTreeNodeProvider.CreateNode(settings, rootNode, "Property1"); + + Assert.AreEqual("Property1", treeNode.Member.Name); + Assert.AreEqual(null, treeNode.Member.Info); + Assert.IsTrue(treeNode.Member.GetType() == typeof(CustomDifferenceTreeNodeMember)); + Assert.IsTrue(treeNode.Ancestor == rootNode); + } + + [Test] + public void TestThrowDifferenceTreeBuilderNotImplementedException() + { + var factory = new CustomComparersFactory(); + var comparer = factory.GetObjectsComparer(); + var rootNode = DifferenceTreeNodeProvider.CreateRootNode(); + Assert.Throws(() => comparer.TryBuildDifferenceTree("hello", "hi", rootNode).ToArray()); + } + + [Test] + public void TestThrowDifferenceTreeBuilderNotImplemented_ConfigureListComparison() + { + var settings = new ComparisonSettings(); + + settings.ConfigureListComparison(); + + var factory = new CustomComparersFactory(); + var comparer = factory.GetObjectsComparer(settings); + + var a1 = new A { ClassB = new B() }; + var a2 = new A { ClassB = new B() }; + + Assert.Throws(() => comparer.CalculateDifferences(a1, a2).ToArray()); + } + + [Test] + public void TestThrowDifferenceTreeBuilderNotImplemented_ConfigureDifference() + { + var settings = new ComparisonSettings(); + + settings.ConfigureDifference(false); + + var factory = new CustomComparersFactory(); + var comparer = factory.GetObjectsComparer(settings); + + var a1 = new A { ClassB = new B() }; + var a2 = new A { ClassB = new B() }; + + Assert.Throws(() => comparer.CalculateDifferences(a1, a2).ToArray()); + } + + [Test] + public void TestThrowDifferenceTreeBuilderNotImplemented_ConfigureDifferenceTree() + { + var settings = new ComparisonSettings(); + + settings.ConfigureDifferenceTree((_, options) => { }); + + var factory = new CustomComparersFactory(); + var comparer = factory.GetObjectsComparer(settings); + + var a1 = new A { ClassB = new B() }; + var a2 = new A { ClassB = new B() }; + + Assert.Throws(() => comparer.CalculateDifferences(a1, a2).ToArray()); + } + + [Test] + public void TestThrowDifferenceTreeBuilderNotImplemented_ConfigureDifferencePath() + { + var settings = new ComparisonSettings(); + + settings.ConfigureDifferencePath((_, options) => { }); + + var factory = new CustomComparersFactory(); + var comparer = factory.GetObjectsComparer(settings); + + var a1 = new A { ClassB = new B() }; + var a2 = new A { ClassB = new B() }; + + Assert.Throws(() => comparer.CalculateDifferences(a1, a2).ToArray()); + } + + [Test] + public void EnumerateConditional() + { + var list = new List { 6, 8, 79, 3, 45, 9 }; + + var enumerateConditional = EnumerateConditionalExt( + list, + moveNextItem: () => true, + completed: () => Debug.WriteLine("Completed")); + + foreach (var item in enumerateConditional) + { + Debug.WriteLine(item); + } + } + + [Test] + public void EnumerateConditional_Completed() + { + var list = new List { 6, 8, 79, 3, 45, 9 }; + bool completed = false; + int? lastElement = null; + + list.EnumerateConditional( + element => + { + lastElement = element; + return true; + }, + () => completed = true); + + Assert.AreEqual(9, lastElement); + Assert.AreEqual(true, completed); + } + + [Test] + public void EnumerateConditional_FetchFirst_NotCompleted() + { + var list = new List { 6, 8, 79, 3, 45, 9 }; + bool completed = false; + int? firstElement = null; + + list.EnumerateConditional( + element => + { + firstElement = element; + return false; + }, + () => completed = true); + + Assert.AreEqual(6, firstElement); + Assert.AreEqual(false, completed); + } + + [Test] + public void CompareIntArrayUnequalListEnabled() + { + var a1 = new int[] { 3, 2, 1 }; + var a2 = new int[] { 1, 2, 3, 4 }; + + var settings = new ComparisonSettings(); + settings.ConfigureListComparison(compareUnequalLists: true); + var comparer = new Comparer(settings); + + var diffs = comparer.CalculateDifferences(a1, a2); + + foreach (var item in diffs) + { + System.Diagnostics.Debug.WriteLine(item); + } + } + + protected IEnumerable EnumerateConditionalExt(IEnumerable enumerable, Func moveNextItem, Action completed = null) + { + var enumerator = enumerable.GetEnumerator(); + + while (moveNextItem()) + { + if (enumerator.MoveNext()) + { + yield return enumerator.Current; + } + else + { + completed?.Invoke(); + break; + } + } + } + } + + class CustomComparersFactory : ComparersFactory + { + public override IComparer GetObjectsComparer(ComparisonSettings settings = null, BaseComparer parentComparer = null) + { + if (typeof(T) == typeof(B)) + { + return (IComparer)new CustomClassBComparer(settings, parentComparer, this); + } + + if (typeof(T) == typeof(string)) + { + return (IComparer)new CustomStringComparer(settings, parentComparer, this); + } + + return base.GetObjectsComparer(settings, parentComparer); + } + } + + class CustomStringComparer : AbstractComparer + { + public CustomStringComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) : base(settings, parentComparer, factory) + { + } + + public override IEnumerable CalculateDifferences(string obj1, string obj2) + { + //var comparer = Factory.GetObjectsComparer(obj1.GetType()); + //return comparer.CalculateDifferences(obj1, obj2); + throw new NotImplementedException(); + } + } + + class CustomClassBComparer : AbstractComparer + { + public CustomClassBComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) : base(settings, parentComparer, factory) + { + } + + public override IEnumerable CalculateDifferences(B obj1, B obj2) + { + throw new NotImplementedException(); + } + } + + class CustomDifferenceTreeNode : DifferenceTreeNodeBase + { + public CustomDifferenceTreeNode(IDifferenceTreeNodeMember member = null, IDifferenceTreeNode ancestor = null) : base(member, ancestor) + { + + } + } + + class CustomDifferenceTreeNodeMember : IDifferenceTreeNodeMember + { + public CustomDifferenceTreeNodeMember(string memberName) + { + Name = memberName; + } + public MemberInfo Info => null; + + public string Name { get; } + } + + //Test commit. +} diff --git a/ObjectsComparer/ObjectsComparer.Tests/TestClasses/A.cs b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/A.cs index cc5fee7..7b588c0 100644 --- a/ObjectsComparer/ObjectsComparer.Tests/TestClasses/A.cs +++ b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/A.cs @@ -30,6 +30,8 @@ internal class A public int[] IntArray { get; set; } + public int[] IntArray2 { get; set; } + public B[] ArrayOfB { get; set; } public Collection CollectionOfB { get; set; } @@ -38,6 +40,8 @@ internal class A public List ListOfB { get; set; } + public List ListOfC { get; set; } + public Dictionary DictionaryOfB { get; set; } public CollectionOfB ClassImplementsCollectionOfB { get; set; } diff --git a/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Address.cs b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Address.cs new file mode 100644 index 0000000..e3d90c5 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Address.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace ObjectsComparer.Tests.TestClasses +{ + public class Address + { + public int Id { get; set; } + + [Description("Aglomerace (město)")] + public string City { get; set; } + + [Description("Země (stát)")] + public string Country { get; set; } + + public string State { get; set; } + + public string PostalCode { get; set; } + + public string Street { get; set; } + } +} diff --git a/ObjectsComparer/ObjectsComparer.Tests/TestClasses/B.cs b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/B.cs index ed42975..8c03dd6 100644 --- a/ObjectsComparer/ObjectsComparer.Tests/TestClasses/B.cs +++ b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/B.cs @@ -3,5 +3,9 @@ public class B { public string Property1 { get; set; } + + public string Property2 { get; set; } + + public int? Id { get; set; } } } diff --git a/ObjectsComparer/ObjectsComparer.Tests/TestClasses/C.cs b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/C.cs new file mode 100644 index 0000000..541f4c7 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/C.cs @@ -0,0 +1,13 @@ +namespace ObjectsComparer.Tests.TestClasses +{ + public class C + { + public string Property1 { get; set; } + + public string Property2 { get; set; } + + public string Key { get; set; } + + public string Property3 { get; set; } + } +} diff --git a/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Customer.cs b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Customer.cs new file mode 100644 index 0000000..43247d8 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Customer.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace ObjectsComparer.Tests.TestClasses +{ + public class Customer + { + public Person Person { get; set; } + } +} diff --git a/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Person.cs b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Person.cs new file mode 100644 index 0000000..6b8e100 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Person.cs @@ -0,0 +1,22 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace ObjectsComparer.Tests.TestClasses +{ + public class Person + { + public string FirstName { get; set; } + + public string LastName { get; set; } + + public System.DateTime? Birthdate { get; set; } + + public string PhoneNumber { get; set; } + + [Description("Kolekce adres")] + public List
ListOfAddress1 { get; set; } + + public List
ListOfAddress2 { get; set; } + } +} diff --git a/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Student.cs b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Student.cs new file mode 100644 index 0000000..ef01419 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Tests/TestClasses/Student.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace ObjectsComparer.Tests.TestClasses +{ + public class Student + { + [Description("Člověk")] + public Person Person { get; set; } + } +} diff --git a/ObjectsComparer/ObjectsComparer.Tests/Utils/DifferenceExtensions.cs b/ObjectsComparer/ObjectsComparer.Tests/Utils/DifferenceExtensions.cs new file mode 100644 index 0000000..2be7ad2 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Tests/Utils/DifferenceExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ObjectsComparer.Tests.Utils +{ + internal class DifferenceEqualityComparer : EqualityComparer + { + public override bool Equals(Difference b1, Difference b2) + { + if (b1 == null && b2 == null) + { + return true; + } + else if (b1 == null || b2 == null) + { + return false; + } + + return (b1.DifferenceType == b2.DifferenceType && b1.MemberPath == b2.MemberPath && b1.Value1 == b2.Value1 && b1.Value2 == b2.Value2); + } + + public override int GetHashCode(Difference obj) + { + var memberPath = obj.MemberPath ?? string.Empty; + var value1 = obj.Value1 ?? string.Empty; + var value2 = obj.Value2 ?? string.Empty; + var hcode = obj.DifferenceType.GetHashCode() ^ memberPath.GetHashCode() ^ value1.GetHashCode() ^ value2.GetHashCode(); + + return hcode.GetHashCode(); + } + } + + public static class DifferenceExtensions + { + public static bool AreEquivalent(this IEnumerable diffs1, IEnumerable diffs2) + { + return diffs1 + .Except(diffs2, new DifferenceEqualityComparer()) + .Any() == false; + } + } +} diff --git a/ObjectsComparer/ObjectsComparer.Tests/Utils/DifferenceTreeNodeSerializationExtensions.cs b/ObjectsComparer/ObjectsComparer.Tests/Utils/DifferenceTreeNodeSerializationExtensions.cs new file mode 100644 index 0000000..75b423e --- /dev/null +++ b/ObjectsComparer/ObjectsComparer.Tests/Utils/DifferenceTreeNodeSerializationExtensions.cs @@ -0,0 +1,132 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace ObjectsComparer.Tests.Utils +{ + internal static class DifferenceTreeNodeSerializationExtensions + { + /// + /// Serializes object to json string. + /// + /// + /// + /// + /// + public static string ToJson(this DifferenceTreeNode differenceTreeNode, bool skipEmptyList = true, bool skipNullReference = true) + { + return SerializeDifferenceTreeNode(differenceTreeNode, skipEmptyList, skipNullReference); + } + + static string SerializeDifferenceTreeNode(DifferenceTreeNode node, bool skipEmptyList, bool skipNullReference) + { + var settings = new JsonSerializerSettings() + { + ContractResolver = new DifferenceTreeNodeContractResolver(skipEmptyList, skipNullReference), + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + }; + settings.Converters.Add(new StringEnumConverter { CamelCaseText = false }); + settings.Converters.Add(new MemberInfoConverter()); + + return JsonConvert.SerializeObject(node, Formatting.Indented, settings); + } + + /// + /// Converts object to JSON. + /// + class MemberInfoConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, MemberInfo value, JsonSerializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName(nameof(MemberInfo.Name)); + writer.WriteValue(value.Name); + writer.WritePropertyName(nameof(MemberInfo.DeclaringType)); + writer.WriteValue(value.DeclaringType.FullName); + writer.WriteEndObject(); + } + + public override MemberInfo ReadJson(JsonReader reader, Type objectType, MemberInfo existingValue, bool hasExistingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override bool CanRead => false; + } + + class DifferenceTreeNodeContractResolver : DefaultContractResolver + { + readonly bool _skipEmptyList; + readonly bool _skipNullReference; + + public DifferenceTreeNodeContractResolver(bool skipEmptyList = true, bool skipNullReference = true) + { + _skipEmptyList = skipEmptyList; + _skipNullReference = skipNullReference; + } + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + JsonProperty property = base.CreateProperty(member, memberSerialization); + + if (property.DeclaringType == typeof(DifferenceTreeNode)) + { + property.ShouldSerialize = + instance => + { + DifferenceTreeNode treeNode = (DifferenceTreeNode)instance; + + if (property.PropertyName == nameof(DifferenceTreeNode.Descendants)) + { + return _skipEmptyList == false || treeNode.Descendants.Any(); + } + + if (property.PropertyName == nameof(DifferenceTreeNode.Differences)) + { + return _skipEmptyList == false || treeNode.Differences.Any(); + } + + if (property.PropertyName == nameof(DifferenceTreeNode.Member)) + { + return _skipNullReference == false || treeNode.Member != null; + } + + if (property.PropertyName == nameof(DifferenceTreeNode.Ancestor)) + { + return _skipNullReference == false || treeNode.Ancestor != null; + } + + return true; + }; + + //if (property.PropertyName == nameof(DifferenceTreeNode.Ancestor)) + //{ + // property.ValueProvider = new AncestorValueProvider(); + //} + } + + return property; + } + } + + //class AncestorValueProvider : IValueProvider + //{ + // public object GetValue(object target) + // { + // var ancestor = (target as DifferenceTreeNode).Ancestor; + // var newAncestor = new DifferenceTreeNode(member: ancestor?.Member); + // return newAncestor; + // } + + // public void SetValue(object target, object value) + // { + // throw new NotImplementedException(); + // } + //} + } +} diff --git a/ObjectsComparer/ObjectsComparer/BaseComparer.cs b/ObjectsComparer/ObjectsComparer/BaseComparer.cs index 0366f09..e7d379e 100644 --- a/ObjectsComparer/ObjectsComparer/BaseComparer.cs +++ b/ObjectsComparer/ObjectsComparer/BaseComparer.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using System.Reflection; using ObjectsComparer.Utils; +using ObjectsComparer.DifferenceTreeExtensions; namespace ObjectsComparer { @@ -139,9 +140,9 @@ public void AddComparerOverride(string memberName, IValueComparer valueComparer, /// /// Sets . + public void SetDefaultComparer(IValueComparer valueComparer) /// /// Value Comparer. - public void SetDefaultComparer(IValueComparer valueComparer) { DefaultValueComparer = valueComparer ?? throw new ArgumentNullException(nameof(valueComparer)); } @@ -183,5 +184,79 @@ public void IgnoreMember(Func filter) { OverridesCollection.AddComparer(DoNotCompareValueComparer.Instance, filter); } + + + /// + /// Adds an to the end of the 's . + /// + /// The instance. + [Obsolete("Use 2. method", true)] + protected virtual DifferenceLocation AddDifferenceToTree(Difference difference, IDifferenceTreeNode differenceTreeNode) + { + if (difference is null) + { + throw new ArgumentNullException(nameof(difference)); + } + + if (differenceTreeNode is null) + { + throw new ArgumentNullException(nameof(differenceTreeNode)); + } + + differenceTreeNode.AddDifference(difference); + + return new DifferenceLocation(difference, differenceTreeNode); + } + + protected virtual DifferenceLocation AddDifferenceToTree(IDifferenceTreeNode differenceTreeNode, string memberPath, string value1, string value2, + DifferenceTypes differenceType = DifferenceTypes.ValueMismatch, object rawValue1 = null, object rawValue2 = null) + { + var difference = CreateDifference(differenceTreeNode, memberPath, value1, value2, differenceType, rawValue1, rawValue2); + differenceTreeNode.AddDifference(difference); + + return new DifferenceLocation(difference, differenceTreeNode); + } + + protected virtual Difference CreateDifference(IDifferenceTreeNode differenceTreeNode, string memberPath, string value1, string value2, + DifferenceTypes differenceType = DifferenceTypes.ValueMismatch, object rawValue1 = null, object rawValue2 = null) + { + if (differenceTreeNode is null) + { + throw new ArgumentNullException(nameof(differenceTreeNode)); + } + + var differenceOptions = DifferenceOptions.Default(); + var defaultDifference = new Difference(memberPath, value1, value2, differenceType); + + Settings.DifferenceOptionsAction?.Invoke(differenceTreeNode, differenceOptions); + + if (differenceOptions.DifferenceFactory == null) + { + return defaultDifference; + } + + var customDifference = differenceOptions.DifferenceFactory(new CreateDifferenceArgs(defaultDifference, rawValue1, rawValue2)); + + if (customDifference == null) + { + throw new NullReferenceException("DifferenceFactory returned null."); + } + + return customDifference; + } + + protected virtual void InsertPathToDifference(Difference difference, string defaultRootElementPath, IDifferenceTreeNode rootNode, IDifferenceTreeNode differenceNode) + { + var differencePathOptions = DifferencePathOptions.Default(); + + Settings.DifferencePathOptionsAction?.Invoke(rootNode, differencePathOptions); + + if (differencePathOptions.InsertPathFactory != null) + { + defaultRootElementPath = differencePathOptions.InsertPathFactory(new InsertPathFactoryArgs(defaultRootElementPath, differenceNode)); + } + + difference.InsertPath(defaultRootElementPath); + } } } \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/Comparer.cs b/ObjectsComparer/ObjectsComparer/Comparer.cs index 7b2684f..231d6a2 100644 --- a/ObjectsComparer/ObjectsComparer/Comparer.cs +++ b/ObjectsComparer/ObjectsComparer/Comparer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; namespace ObjectsComparer @@ -9,7 +10,7 @@ namespace ObjectsComparer /// /// Compares objects. /// - public class Comparer : AbstractComparer + public class Comparer : AbstractComparer, IDifferenceTreeBuilder { private static string CalculateDifferencesMethodName { @@ -17,6 +18,11 @@ private static string CalculateDifferencesMethodName get { return MemberInfoExtensions.GetMethodName>(x => x.CalculateDifferences(null, null)); } } + private static string BuildDifferenceTreeMethodName + { + get { return MemberInfoExtensions.GetMethodName>(x => x.BuildDifferenceTree(null, null, null)); } + } + /// /// Initializes a new instance of the class. /// @@ -27,23 +33,78 @@ public Comparer(ComparisonSettings settings = null, BaseComparer parentComparer { } - /// - /// Calculates list of differences between objects. - /// - /// Type. - /// Object 1. - /// Object 2. - /// List of differences between objects. public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) { - var objectsComparerMethod = typeof(IComparersFactory).GetTypeInfo().GetMethods().First(m => m.IsGenericMethod); - var objectsComparerGenericMethod = objectsComparerMethod.MakeGenericMethod(type); - var comparer = objectsComparerGenericMethod.Invoke(Factory, new object[] { Settings, this }); - var genericType = typeof(IComparer<>).MakeGenericType(type); - var method = genericType.GetTypeInfo().GetMethod(CalculateDifferencesMethodName, new[] { type, type }); + return AsDifferenceTreeBuilder().BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLoccation => differenceLoccation.Difference); + } + + IDifferenceTreeBuilder AsDifferenceTreeBuilder() => this; + + IEnumerable IDifferenceTreeBuilder.BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode) + { + if (differenceTreeNode is null) + { + throw new ArgumentNullException(nameof(differenceTreeNode)); + } + + var getObjectsComparerMethod = typeof(IComparersFactory).GetTypeInfo().GetMethods().First(m => m.IsGenericMethod); + var getObjectsComparerGenericMethod = getObjectsComparerMethod.MakeGenericMethod(type); + var comparer = getObjectsComparerGenericMethod.Invoke(Factory, new object[] { Settings, this }); + + bool comparerIsDifferenceTreeBuilderT = comparer.GetType().GetTypeInfo().GetInterfaces() + .Any(intft => intft.GetTypeInfo().IsGenericType && intft.GetGenericTypeDefinition() == typeof(IDifferenceTreeBuilder<>)); + + if (comparerIsDifferenceTreeBuilderT == false) + { + if (comparer is IDifferenceTreeBuilder differenceTreeBuilder) + { + var diffLocationList = differenceTreeBuilder.BuildDifferenceTree(type, obj1, obj2, differenceTreeNode); + + foreach (var diffLocation in diffLocationList) + { + yield return diffLocation; + } + + yield break; + } + + DifferenceTreeBuilderExtensions.ThrowDifferenceTreeBuilderNotImplemented(differenceTreeNode, Settings, comparer, $"{nameof(IDifferenceTreeBuilder)}<{type.FullName}>"); + } + + var genericType = comparerIsDifferenceTreeBuilderT ? typeof(IDifferenceTreeBuilder<>).MakeGenericType(type) : typeof(IComparer<>).MakeGenericType(type); + var genericMethodName = comparerIsDifferenceTreeBuilderT ? BuildDifferenceTreeMethodName : CalculateDifferencesMethodName; + var genericMethodParameterTypes = comparerIsDifferenceTreeBuilderT ? new[] { type, type, typeof(IDifferenceTreeNode) } : new[] { type, type }; + var genericMethod = genericType.GetTypeInfo().GetMethod(genericMethodName, genericMethodParameterTypes); + var genericMethodParameters = comparerIsDifferenceTreeBuilderT ? new[] { obj1, obj2, differenceTreeNode } : new[] { obj1, obj2 }; // ReSharper disable once PossibleNullReferenceException - return (IEnumerable)method.Invoke(comparer, new[] { obj1, obj2 }); + //return (IEnumerable)genericMethod.Invoke(comparer, genericMethodParameters); + + var returnValue = genericMethod.Invoke(comparer, genericMethodParameters); + + if (returnValue is IEnumerable differenceLocationList) + { + foreach (var differenceLocation in differenceLocationList) + { + yield return differenceLocation; + } + + yield break; + } + + if (returnValue is IEnumerable differenceList) + { + foreach (var difference in differenceList) + { + yield return new DifferenceLocation(difference); + } + + yield break; + } + + //TODO: + throw new NotImplementedException(""); } } } diff --git a/ObjectsComparer/ObjectsComparer/Comparer~1.cs b/ObjectsComparer/ObjectsComparer/Comparer~1.cs index f1c75df..fc1bc17 100644 --- a/ObjectsComparer/ObjectsComparer/Comparer~1.cs +++ b/ObjectsComparer/ObjectsComparer/Comparer~1.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Text; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; using ObjectsComparer.Attributes; @@ -11,7 +12,7 @@ namespace ObjectsComparer /// /// Compares objects of type . /// - public class Comparer : AbstractComparer + public class Comparer : AbstractComparer, IDifferenceTreeBuilder { private readonly List _members; private readonly List _conditionalComparers; @@ -56,10 +57,21 @@ public Comparer(ComparisonSettings settings = null, BaseComparer parentComparer /// List of differences between objects. public override IEnumerable CalculateDifferences(T obj1, T obj2) { - return CalculateDifferences(obj1, obj2, null); + return CalculateDifferences(obj1, obj2, memberInfo: null); + } + + IEnumerable IDifferenceTreeBuilder.BuildDifferenceTree(T obj1, T obj2, IDifferenceTreeNode differenceTreeNode) + { + return BuildDifferenceTree(obj1, obj2, memberInfo: null, differenceTreeNode); } internal IEnumerable CalculateDifferences(T obj1, T obj2, MemberInfo memberInfo) + { + return BuildDifferenceTree(obj1, obj2, memberInfo, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLocation => differenceLocation.Difference); + } + + IEnumerable BuildDifferenceTree(T obj1, T obj2, MemberInfo memberInfo, IDifferenceTreeNode differenceTreeNode) { var comparer = memberInfo != null ? OverridesCollection.GetComparer(memberInfo) @@ -71,9 +83,7 @@ internal IEnumerable CalculateDifferences(T obj1, T obj2, MemberInfo comparer = comparer ?? DefaultValueComparer; if (!comparer.Compare(obj1, obj2, Settings)) { - yield return - new Difference(string.Empty, comparer.ToString(obj1), - comparer.ToString(obj2)); + yield return AddDifferenceToTree(differenceTreeNode, string.Empty, comparer.ToString(obj1), comparer.ToString(obj2), DifferenceTypes.ValueMismatch, obj1, obj2); } yield break; @@ -82,7 +92,7 @@ internal IEnumerable CalculateDifferences(T obj1, T obj2, MemberInfo var conditionalComparer = _conditionalComparers.FirstOrDefault(c => c.IsMatch(typeof(T), obj1, obj2)); if (conditionalComparer != null) { - foreach (var difference in conditionalComparer.CalculateDifferences(typeof(T), obj1, obj2)) + foreach (var difference in conditionalComparer.TryBuildDifferenceTree(typeof(T), obj1, obj2, differenceTreeNode)) { yield return difference; } @@ -97,7 +107,7 @@ internal IEnumerable CalculateDifferences(T obj1, T obj2, MemberInfo { if (!DefaultValueComparer.Compare(obj1, obj2, Settings)) { - yield return new Difference(string.Empty, DefaultValueComparer.ToString(obj1), DefaultValueComparer.ToString(obj2)); + yield return AddDifferenceToTree(differenceTreeNode, string.Empty, DefaultValueComparer.ToString(obj1), DefaultValueComparer.ToString(obj2), DifferenceTypes.ValueMismatch, obj1, obj2); } yield break; @@ -124,6 +134,8 @@ internal IEnumerable CalculateDifferences(T obj1, T obj2, MemberInfo continue; } + var memberNode = DifferenceTreeNodeProvider.CreateNode(Settings, differenceTreeNode, member); + var valueComparer = DefaultValueComparer; var hasCustomComparer = false; @@ -139,9 +151,10 @@ internal IEnumerable CalculateDifferences(T obj1, T obj2, MemberInfo { var objectDataComparer = Factory.GetObjectsComparer(type, Settings, this); - foreach (var failure in objectDataComparer.CalculateDifferences(type, value1, value2)) + foreach (var failure in objectDataComparer.TryBuildDifferenceTree(type, value1, value2, memberNode)) { - yield return failure.InsertPath(member.Name); + InsertPathToDifference(failure.Difference, member.Name, memberNode, failure.TreeNode); + yield return failure; } continue; @@ -149,7 +162,7 @@ internal IEnumerable CalculateDifferences(T obj1, T obj2, MemberInfo if (!valueComparer.Compare(value1, value2, Settings)) { - yield return new Difference(member.Name, valueComparer.ToString(value1), valueComparer.ToString(value2)); + yield return AddDifferenceToTree(memberNode, member.Name, valueComparer.ToString(value1), valueComparer.ToString(value2), DifferenceTypes.ValueMismatch, value1, value2); } } } @@ -182,5 +195,7 @@ private List GetProperties(Type type, List processedTypes) return properties; } + + } } diff --git a/ObjectsComparer/ObjectsComparer/ComparisonSettings.cs b/ObjectsComparer/ObjectsComparer/ComparisonSettings.cs index f8d4974..bb34818 100644 --- a/ObjectsComparer/ObjectsComparer/ComparisonSettings.cs +++ b/ObjectsComparer/ObjectsComparer/ComparisonSettings.cs @@ -1,12 +1,14 @@ using System; using System.Collections.Generic; +using System.Reflection; +using System.Collections; namespace ObjectsComparer { /// /// Configuration for Objects Comparer. /// - public class ComparisonSettings + public partial class ComparisonSettings { /// /// If true, all members which are not primitive types, do not have custom comparison rule and @@ -15,7 +17,7 @@ public class ComparisonSettings public bool RecursiveComparison { get; set; } /// - /// If true, empty and null values will be considered as equal values. False by default. + /// If true, empty and null values will be considered as equal values. False by default. /// public bool EmptyAndNullEnumerablesEqual { get; set; } @@ -66,5 +68,152 @@ public T GetCustomSetting(string key = null) throw new KeyNotFoundException(); } + + public Action ListComparisonOptionsAction { get; private set; } = null; + + /// + /// Configures list comparison behavior, especially the type of the comparison. For more info, see . + /// The term list has a general meaning here and includes almost all Enumerable objects. + /// + /// + /// First parameter: Current list node. + /// + public ComparisonSettings ConfigureListComparison(Action comparisonOptions) + { + if (comparisonOptions is null) + { + throw new ArgumentNullException(nameof(comparisonOptions)); + } + + ListComparisonOptionsAction = comparisonOptions; + + return this; + } + + /// + /// Configures list comparison behavior, especially the type of comparison. For more info, see . + /// The term list has a general meaning here and includes almost all Enumerable objects. + /// + /// See . + public ComparisonSettings ConfigureListComparison(Action comparisonOptions) + { + ConfigureListComparison((_, options) => comparisonOptions(options)); + + return this; + } + + /// + /// Configures the type of list comparison and whether to compare unequal lists. For more info, see . + /// The term list has a general meaning here and includes almost all Enumerable objects. + /// + /// + /// True value is shortcut for operation. + /// False value is shortcut for operation. Default value = false. + /// + /// + /// Shortcut for operation. Default value = false. + /// + public ComparisonSettings ConfigureListComparison(bool compareElementsByKey = false, bool compareUnequalLists = false) + { + ConfigureListComparison(options => + { + options.CompareUnequalLists(compareUnequalLists); + + if (compareElementsByKey) + { + options.CompareElementsByKey(); + } + }); + + return this; + } + + public Action DifferenceTreeOptionsAction { get; private set; } + + /// + /// Configures creation of the instance, see . + /// + /// + /// First parameter: The ancestor member the tree node is configured for. + /// + public ComparisonSettings ConfigureDifferenceTree(Action options) + { + DifferenceTreeOptionsAction = options ?? throw new ArgumentNullException(nameof(options)); + + return this; + } + + public Action DifferenceOptionsAction; + + /// + /// Configures creation of the instance, see . + /// + /// + /// First parameter: The member the difference is configured for. + /// + public ComparisonSettings ConfigureDifference(Action differenceOptions) + { + DifferenceOptionsAction = differenceOptions ?? throw new ArgumentNullException(nameof(differenceOptions)); + + return this; + } + + /// + /// Configures creation of the instance. + /// + public ComparisonSettings ConfigureDifference(bool includeRawValues) + { + ConfigureDifference((_, options) => options.IncludeRawValues(includeRawValues)); + + return this; + } + + /// + /// Wraps , and + /// by applying the argument to them. + /// + /// + /// Customizes the , .
+ /// First parameter: It can be null in some cases, for example, when it represents the root of the comparison. + /// + /// Whether raw values should be included in the instance. + public ComparisonSettings ConfigureDifferences(Func memberNameProvider, bool includeRawValues = false) + { + ConfigureDifferenceTree((ancestor, options) => + options.UseDifferenceTreeNodeMemberFactory(defaultMember => + new DifferenceTreeNodeMember( + defaultMember?.Info, + memberNameProvider(defaultMember?.Info)))); + + ConfigureDifference((currentMember, options) => + options.UseDifferenceFactory(args => + new Difference( + memberPath: memberNameProvider(currentMember.Member.Info), + args.DefaultDifference.Value1, + args.DefaultDifference.Value2, + args.DefaultDifference.DifferenceType, + rawValue1: includeRawValues ? args.RawValue1 : null, + rawValue2: includeRawValues ? args.RawValue2 : null))); + + ConfigureDifferencePath((ancestor, options) => + options.UseInsertPathFactory(args => memberNameProvider(ancestor.Member.Info))); + + return this; + } + + public Action DifferencePathOptionsAction; + + /// + /// Configures the insertion into the difference path, see . + /// + /// + /// First parameter: The parent of the member to which the path is inserted. + /// + public ComparisonSettings ConfigureDifferencePath(Action options) + { + DifferencePathOptionsAction = options ?? throw new ArgumentNullException(nameof(options)); + + return this; + } } } diff --git a/ObjectsComparer/ObjectsComparer/CreateDifferenceArgs.cs b/ObjectsComparer/ObjectsComparer/CreateDifferenceArgs.cs new file mode 100644 index 0000000..067bbb0 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/CreateDifferenceArgs.cs @@ -0,0 +1,26 @@ +using System; + +namespace ObjectsComparer +{ + /// + /// Arguments for the factory. + /// + public class CreateDifferenceArgs + { + public object RawValue1 { get; } + + public object RawValue2 { get; } + + /// + /// The default difference that a factory can return as its fallback. + /// + public Difference DefaultDifference { get; } + + public CreateDifferenceArgs(Difference defaultDifference, object rawValue1 = null, object rawValue2 = null) + { + DefaultDifference = defaultDifference ?? throw new ArgumentNullException(nameof(defaultDifference)); + RawValue1 = rawValue1; + RawValue2 = rawValue2; + } + } +} diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/AbstractDynamicObjectsComprer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/AbstractDynamicObjectsComprer.cs index ac08834..cb6655d 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/AbstractDynamicObjectsComprer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/AbstractDynamicObjectsComprer.cs @@ -2,17 +2,34 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; namespace ObjectsComparer { - internal abstract class AbstractDynamicObjectsComprer: AbstractComparer, IComparerWithCondition + internal abstract class AbstractDynamicObjectsComprer: AbstractComparer, IComparerWithCondition, IDifferenceTreeBuilder, IDifferenceTreeBuilder { protected AbstractDynamicObjectsComprer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) : base(settings, parentComparer, factory) { } public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) + { + return AsDifferenceTreeBuilder().BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLocation => differenceLocation.Difference); + } + + IEnumerable IDifferenceTreeBuilder.BuildDifferenceTree(T obj1, T obj2, IDifferenceTreeNode differenceTreeNode) + { + return AsDifferenceTreeBuilder().BuildDifferenceTree(typeof(T), obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)); + } + + IDifferenceTreeBuilder AsDifferenceTreeBuilder() + { + return this; + } + + IEnumerable IDifferenceTreeBuilder.BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode) { var castedObject1 = (T)obj1; var castedObject2 = (T)obj2; @@ -26,17 +43,23 @@ public override IEnumerable CalculateDifferences(Type type, object o var existsInObject1 = propertyKeys1.Contains(propertyKey); var existsInObject2 = propertyKeys2.Contains(propertyKey); object value1 = null; + MemberInfo member1 = null; if (existsInObject1) { TryGetMemberValue(castedObject1, propertyKey, out value1); + TryGetMember(castedObject1, propertyKey, out member1); } object value2 = null; + MemberInfo member2 = null; if (existsInObject2) { TryGetMemberValue(castedObject2, propertyKey, out value2); + TryGetMember(castedObject2, propertyKey, out member2); } + var keyDifferenceTreeNode = DifferenceTreeNodeProvider.CreateNode(Settings, differenceTreeNode, member1 ?? member2, propertyKey); + var propertyType = (value1 ?? value2)?.GetType() ?? typeof(object); var customComparer = OverridesCollection.GetComparer(propertyType) ?? OverridesCollection.GetComparer(propertyKey); @@ -59,15 +82,24 @@ public override IEnumerable CalculateDifferences(Type type, object o { if (!existsInObject1) { - yield return new Difference(propertyKey, string.Empty, valueComparer.ToString(value2), - DifferenceTypes.MissedMemberInFirstObject); + var differenceLocation = AddDifferenceToTree(keyDifferenceTreeNode, propertyKey, string.Empty, valueComparer.ToString(value2), DifferenceTypes.MissedMemberInFirstObject, null, value2); + + yield return differenceLocation; continue; } if (!existsInObject2) { - yield return new Difference(propertyKey, valueComparer.ToString(value1), string.Empty, - DifferenceTypes.MissedMemberInSecondObject); + var differenceLocation = AddDifferenceToTree( + keyDifferenceTreeNode, + propertyKey, + valueComparer.ToString(value1), + string.Empty, + DifferenceTypes.MissedMemberInSecondObject, + value1, + null); + + yield return differenceLocation; continue; } } @@ -77,8 +109,10 @@ public override IEnumerable CalculateDifferences(Type type, object o var valueComparer2 = OverridesCollection.GetComparer(value2.GetType()) ?? OverridesCollection.GetComparer(propertyKey) ?? DefaultValueComparer; - yield return new Difference(propertyKey, valueComparer.ToString(value1), valueComparer2.ToString(value2), - DifferenceTypes.TypeMismatch); + + var differenceLocation = AddDifferenceToTree(keyDifferenceTreeNode, propertyKey, valueComparer.ToString(value1), valueComparer2.ToString(value2), DifferenceTypes.TypeMismatch, value1, value2); + + yield return differenceLocation; continue; } @@ -89,8 +123,10 @@ public override IEnumerable CalculateDifferences(Type type, object o var valueComparer2 = value2 != null ? OverridesCollection.GetComparer(value2.GetType()) ?? OverridesCollection.GetComparer(propertyKey) ?? DefaultValueComparer : DefaultValueComparer; - yield return new Difference(propertyKey, valueComparer.ToString(value1), valueComparer2.ToString(value2), - DifferenceTypes.TypeMismatch); + + var differenceLocation = AddDifferenceToTree(keyDifferenceTreeNode, propertyKey, valueComparer.ToString(value1), valueComparer2.ToString(value2), DifferenceTypes.TypeMismatch, value1, value2); + + yield return differenceLocation; continue; } @@ -98,20 +134,23 @@ public override IEnumerable CalculateDifferences(Type type, object o { if (!customComparer.Compare(value1, value2, Settings)) { - yield return new Difference(propertyKey, customComparer.ToString(value1), customComparer.ToString(value2)); + var differenceLocation = AddDifferenceToTree(keyDifferenceTreeNode, propertyKey, customComparer.ToString(value1), customComparer.ToString(value2), DifferenceTypes.ValueMismatch, value1, value2); + + yield return differenceLocation; } continue; } var comparer = Factory.GetObjectsComparer(propertyType, Settings, this); - foreach (var failure in comparer.CalculateDifferences(propertyType, value1, value2)) + foreach (var failure in comparer.TryBuildDifferenceTree(propertyType, value1, value2, keyDifferenceTreeNode)) { - yield return failure.InsertPath(propertyKey); + InsertPathToDifference(failure.Difference, propertyKey, keyDifferenceTreeNode,failure.TreeNode); + yield return failure; } } } - + public abstract bool IsMatch(Type type, object obj1, object obj2); public abstract bool IsStopComparison(Type type, object obj1, object obj2); @@ -121,5 +160,7 @@ public override IEnumerable CalculateDifferences(Type type, object o protected abstract IList GetProperties(T obj); protected abstract bool TryGetMemberValue(T obj, string propertyName, out object value); + + protected abstract bool TryGetMember(T obj, string propertyName, out MemberInfo value); } } \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/AbstractEnumerablesComparer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/AbstractEnumerablesComparer.cs index ac54739..ae42fd9 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/AbstractEnumerablesComparer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/AbstractEnumerablesComparer.cs @@ -1,12 +1,29 @@ using System; +using System.Linq; using System.Collections.Generic; using System.Reflection; using ObjectsComparer.Utils; +using System.Collections; +using ObjectsComparer.DifferenceTreeExtensions; namespace ObjectsComparer { - internal abstract class AbstractEnumerablesComparer: AbstractComparer, IComparerWithCondition + internal abstract class AbstractEnumerablesComparer: AbstractComparer, IComparerWithCondition, IDifferenceTreeBuilder { + ///// + ///// member names that will be skipped from comaprison. + ///// + //static readonly string[] SkipArrayMemberNameList = new string[] + //{ + // nameof(Array.Length), + // "LongLength", + // nameof(Array.Rank), + // "SyncRoot", + // "IsReadOnly", + // "IsFixedSize", + // "IsSynchronized" + //}; + protected AbstractEnumerablesComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) : base(settings, parentComparer, factory) @@ -42,5 +59,7 @@ public virtual bool SkipMember(Type type, MemberInfo member) public abstract override IEnumerable CalculateDifferences(Type type, object obj1, object obj2); public abstract bool IsMatch(Type type, object obj1, object obj2); + + public abstract IEnumerable BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode); } } \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/CompilerGeneratedObjectComparer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/CompilerGeneratedObjectComparer.cs index a034d6c..9d318d0 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/CompilerGeneratedObjectComparer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/CompilerGeneratedObjectComparer.cs @@ -58,5 +58,24 @@ protected override bool TryGetMemberValue(object obj, string propertyName, out o return true; } + + protected override bool TryGetMember(object obj, string propertyName, out MemberInfo value) + { + value = null; + + if (obj == null) + { + return false; + } + + value = obj.GetType().GetTypeInfo().GetProperty(propertyName); + + if (value == null) + { + return false; + } + + return false; + } } } diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/DynamicObjectComparer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/DynamicObjectComparer.cs index f37ce23..1b6c141 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/DynamicObjectComparer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/DynamicObjectComparer.cs @@ -59,5 +59,11 @@ protected override bool TryGetMemberValue(DynamicObject obj, string propertyName return obj.TryGetMember(getBinder, out value); } + + protected override bool TryGetMember(DynamicObject obj, string propertyName, out MemberInfo value) + { + value = null; + return false; + } } } diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparer.cs index 11871cd..b42509f 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparer.cs @@ -1,28 +1,45 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; +using ObjectsComparer.DifferenceTreeExtensions; +using ObjectsComparer.Exceptions; using ObjectsComparer.Utils; namespace ObjectsComparer { - internal class EnumerablesComparer : AbstractComparer, IComparerWithCondition + internal class EnumerablesComparer : EnumerablesComparerBase, IComparerWithCondition, IDifferenceTreeBuilder { - public EnumerablesComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) - : base(settings, parentComparer, factory) + public EnumerablesComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) : base(settings, parentComparer, factory) { } public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) { + return AsDifferenceTreeBuilder().BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLocation => differenceLocation.Difference); + } + + IDifferenceTreeBuilder AsDifferenceTreeBuilder() => this; + + IEnumerable IDifferenceTreeBuilder.BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode listDifferenceTreeNode) + { + Debug.WriteLine($"{GetType().Name}.{nameof(CalculateDifferences)}: {type.Name}"); + + if (listDifferenceTreeNode is null) + { + throw new ArgumentNullException(nameof(listDifferenceTreeNode)); + } + if (!Settings.EmptyAndNullEnumerablesEqual && (obj1 == null || obj2 == null) && obj1 != obj2) { - yield return new Difference("[]", obj1?.ToString() ?? string.Empty, obj2?.ToString() ?? string.Empty); + yield return AddDifferenceToTree(listDifferenceTreeNode, "[]", obj1?.ToString() ?? string.Empty, obj2?.ToString() ?? string.Empty, DifferenceTypes.ValueMismatch, obj1, obj2); yield break; } - + obj1 = obj1 ?? Enumerable.Empty(); obj2 = obj2 ?? Enumerable.Empty(); @@ -42,49 +59,26 @@ public override IEnumerable CalculateDifferences(Type type, object o } var array1 = ((IEnumerable)obj1).Cast().ToArray(); - var array2 = ((IEnumerable)obj2).Cast().ToArray(); + var array2 = ((IEnumerable)obj2).Cast().ToArray(); + + var listComparisonOptions = ListComparisonOptions.Default(); + Settings.ListComparisonOptionsAction?.Invoke(listDifferenceTreeNode, listComparisonOptions); if (array1.Length != array2.Length) { - yield return new Difference("", array1.Length.ToString(), array2.Length.ToString(), - DifferenceTypes.NumberOfElementsMismatch); - yield break; - } + yield return AddDifferenceToTree(listDifferenceTreeNode, "", array1.Length.ToString(), array2.Length.ToString(), DifferenceTypes.NumberOfElementsMismatch, array1, array2); - //ToDo Extract type - for (var i = 0; i < array2.Length; i++) - { - if (array1[i] == null && array2[i] == null) + if (listComparisonOptions.UnequalListsComparisonEnabled == false) { - continue; - } - - var valueComparer1 = array1[i] != null ? OverridesCollection.GetComparer(array1[i].GetType()) ?? DefaultValueComparer : DefaultValueComparer; - var valueComparer2 = array2[i] != null ? OverridesCollection.GetComparer(array2[i].GetType()) ?? DefaultValueComparer : DefaultValueComparer; - - if (array1[i] == null) - { - yield return new Difference($"[{i}]", string.Empty, valueComparer2.ToString(array2[i])); - continue; - } - - if (array2[i] == null) - { - yield return new Difference($"[{i}]", valueComparer1.ToString(array1[i]), string.Empty); - continue; - } - - if (array1[i].GetType() != array2[i].GetType()) - { - yield return new Difference($"[{i}]", valueComparer1.ToString(array1[i]), valueComparer2.ToString(array2[i]), DifferenceTypes.TypeMismatch); - continue; + yield break; } + } - var comparer = Factory.GetObjectsComparer(array1[i].GetType(), Settings, this); - foreach (var failure in comparer.CalculateDifferences(array1[i].GetType(), array1[i], array2[i])) - { - yield return failure.InsertPath($"[{i}]"); - } + var failrues = CalculateDifferences(array1, array2, listDifferenceTreeNode, listComparisonOptions); + + foreach (var failrue in failrues) + { + yield return failrue; } } diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparerBase.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparerBase.cs new file mode 100644 index 0000000..ed2b671 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparerBase.cs @@ -0,0 +1,205 @@ +using ObjectsComparer.DifferenceTreeExtensions; +using ObjectsComparer.Exceptions; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace ObjectsComparer +{ + internal abstract class EnumerablesComparerBase : AbstractComparer + { + public EnumerablesComparerBase(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) : base(settings, parentComparer, factory) + { + } + + /// + /// Selects calculation operation based on the current value of the property. + /// + protected virtual IEnumerable CalculateDifferences(IList list1, IList list2, IDifferenceTreeNode listDifferenceTreeNode, ListComparisonOptions listComparisonOptions) + { + if (listComparisonOptions.ElementSearchMode == ListElementSearchMode.Key) + { + return CalculateDifferencesByKey(list1, list2, listDifferenceTreeNode, listComparisonOptions); + } + else if (listComparisonOptions.ElementSearchMode == ListElementSearchMode.Index) + { + return CalculateDifferencesByIndex(list1, list2, listDifferenceTreeNode, listComparisonOptions); + } + else + { + throw new NotImplementedException($"{listComparisonOptions.ElementSearchMode} not implemented yet."); + } + } + + /// + /// Calculates differences using comparison mode. + /// + protected virtual IEnumerable CalculateDifferencesByKey(IList array1, IList array2, IDifferenceTreeNode listDifferenceTreeNode, ListComparisonOptions listComparisonOptions) + { + Debug.WriteLine($"{GetType().Name}.{nameof(CalculateDifferencesByKey)}: {array1?.GetType().Name}"); + + var keyOptions = ListElementComparisonByKeyOptions.Default(); + listComparisonOptions.KeyOptionsAction?.Invoke(keyOptions); + + for (int element1Index = 0; element1Index < array1.Count(); element1Index++) + { + var element1 = array1[element1Index]; + var elementDifferenceTreeNode = DifferenceTreeNodeProvider.CreateNode(Settings, listDifferenceTreeNode); + + if (element1 == null) + { + if (array2.Any(elm2 => elm2 == null)) + { + continue; + } + + var nullElementIdentifier = keyOptions.GetNullElementIdentifier(new FormatNullElementIdentifierArgs(element1Index)); + + yield return AddDifferenceToTree(elementDifferenceTreeNode, $"[{nullElementIdentifier}]", string.Empty, string.Empty, DifferenceTypes.MissedElementInSecondObject); + continue; + } + + var element1Key = keyOptions.ElementKeyProviderAction(new ListElementKeyProviderArgs(element1)); + + if (element1Key == null) + { + if (keyOptions.ThrowKeyNotFoundEnabled) + { + throw new ElementKeyNotFoundException(element1, elementDifferenceTreeNode); + } + + continue; + } + + var formattedElement1Key = keyOptions.GetFormattedElementKey(new FormatListElementKeyArgs(element1Index, element1Key, element1)); + + if (array2.Any(elm2 => elm2 != null && object.Equals(element1Key, keyOptions.ElementKeyProviderAction(new ListElementKeyProviderArgs(elm2))))) + { + var element2 = array2.First(elm2 => elm2 != null && object.Equals(element1Key, keyOptions.ElementKeyProviderAction(new ListElementKeyProviderArgs(elm2)))); + var comparer = Factory.GetObjectsComparer(element1.GetType(), Settings, this); + + foreach (var failure in comparer.TryBuildDifferenceTree(element1.GetType(), element1, element2, elementDifferenceTreeNode)) + { + failure.Difference.InsertPath($"[{formattedElement1Key}]"); + yield return failure; + } + } + else + { + var valueComparer1 = OverridesCollection.GetComparer(element1.GetType()) ?? DefaultValueComparer; + yield return AddDifferenceToTree(elementDifferenceTreeNode, $"[{formattedElement1Key}]", valueComparer1.ToString(element1), string.Empty, DifferenceTypes.MissedElementInSecondObject, element1); + } + } + + for (int element2Index = 0; element2Index < array2.Count(); element2Index++) + { + var element2 = array2[element2Index]; + var elementDifferenceTreeNode = DifferenceTreeNodeProvider.CreateNode(Settings, listDifferenceTreeNode); + + if (element2 == null) + { + if (array1.Any(elm1 => elm1 == null)) + { + continue; + } + + var nullElementIdentifier = keyOptions.GetNullElementIdentifier(new FormatNullElementIdentifierArgs(element2Index)); + + yield return AddDifferenceToTree(elementDifferenceTreeNode, $"[{nullElementIdentifier}]", string.Empty, string.Empty, DifferenceTypes.MissedElementInFirstObject); + continue; + } + + var element2Key = keyOptions.ElementKeyProviderAction(new ListElementKeyProviderArgs(element2)); + + if (element2Key == null) + { + if (keyOptions.ThrowKeyNotFoundEnabled) + { + throw new ElementKeyNotFoundException(element2, elementDifferenceTreeNode); + } + + continue; + } + + if (array1.Any(elm1 => elm1 != null && object.Equals(element2Key, keyOptions.ElementKeyProviderAction(new ListElementKeyProviderArgs(elm1)))) == false) + { + var formattedElement2Key = keyOptions.GetFormattedElementKey(new FormatListElementKeyArgs(element2Index, element2Key, element2)); + var valueComparer2 = OverridesCollection.GetComparer(element2.GetType()) ?? DefaultValueComparer; + yield return AddDifferenceToTree(elementDifferenceTreeNode, $"[{formattedElement2Key}]", string.Empty, valueComparer2.ToString(element2), DifferenceTypes.MissedElementInFirstObject, null, element2); + } + } + } + + /// + /// Calculates differences using comparison mode. + /// + protected virtual IEnumerable CalculateDifferencesByIndex(IList array1, IList array2, IDifferenceTreeNode listDifferenceTreeNode, ListComparisonOptions listComparisonOptions) + { + Debug.WriteLine($"{GetType().Name}.{nameof(CalculateDifferencesByIndex)}: {array1?.GetType().Name}"); + + int array1Count = array1.Count(); + int array2Count = array2.Count(); + int smallerCount = array1Count <= array2Count ? array1Count : array2Count; + + //ToDo Extract type + for (var i = 0; i < smallerCount; i++) + { + var elementDifferenceTreeNode = DifferenceTreeNodeProvider.CreateNode(Settings, listDifferenceTreeNode); + + if (array1[i] == null && array2[i] == null) + { + continue; + } + + var valueComparer1 = array1[i] != null ? OverridesCollection.GetComparer(array1[i].GetType()) ?? DefaultValueComparer : DefaultValueComparer; + var valueComparer2 = array2[i] != null ? OverridesCollection.GetComparer(array2[i].GetType()) ?? DefaultValueComparer : DefaultValueComparer; + + if (array1[i] == null) + { + yield return AddDifferenceToTree(elementDifferenceTreeNode, $"[{i}]", string.Empty, valueComparer2.ToString(array2[i]), DifferenceTypes.ValueMismatch, null, array2[i]); + continue; + } + + if (array2[i] == null) + { + yield return AddDifferenceToTree(elementDifferenceTreeNode, $"[{i}]", valueComparer1.ToString(array1[i]), string.Empty, DifferenceTypes.ValueMismatch, array1[i]); + continue; + } + + if (array1[i].GetType() != array2[i].GetType()) + { + yield return AddDifferenceToTree(elementDifferenceTreeNode, $"[{i}]", valueComparer1.ToString(array1[i]), valueComparer2.ToString(array2[i]), DifferenceTypes.TypeMismatch, array1[i], array2[i]); + continue; + } + + var comparer = Factory.GetObjectsComparer(array1[i].GetType(), Settings, this); + + foreach (var failure in comparer.TryBuildDifferenceTree(array1[i].GetType(), array1[i], array2[i], elementDifferenceTreeNode)) + { + failure.Difference.InsertPath($"[{i}]"); + yield return failure; + } + } + + //Add a "missed element" difference for each element that is in array1 and that is not in array2 or vice versa. + if (array1Count != array2Count) + { + var largerArray = array1Count > array2Count ? array1 : array2; + + for (int i = smallerCount; i < largerArray.Count(); i++) + { + var valueComparer = largerArray[i] != null ? OverridesCollection.GetComparer(largerArray[i].GetType()) ?? DefaultValueComparer : DefaultValueComparer; + + yield return AddDifferenceToTree(DifferenceTreeNodeProvider.CreateNode(Settings, listDifferenceTreeNode), + memberPath: $"[{i}]", + value1: array1Count > array2Count ? valueComparer.ToString(largerArray[i]) : string.Empty, + value2: array2Count > array1Count ? valueComparer.ToString(largerArray[i]) : string.Empty, + differenceType: array1Count > array2Count ? DifferenceTypes.MissedElementInSecondObject : DifferenceTypes.MissedElementInFirstObject, + array1Count > array2Count ? largerArray[i] : (object)null, + array2Count > array1Count ? largerArray[i] : (object)null); + } + } + } + } +} diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparer~1.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparer~1.cs index 3f32656..2c4c393 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparer~1.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/EnumerablesComparer~1.cs @@ -1,23 +1,37 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; namespace ObjectsComparer { - internal class EnumerablesComparer : AbstractComparer + internal class EnumerablesComparer : EnumerablesComparerBase, IDifferenceTreeBuilder, IDifferenceTreeBuilder { private readonly IComparer _comparer; - public EnumerablesComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) - :base(settings, parentComparer, factory) + public EnumerablesComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) : base(settings, parentComparer, factory) { _comparer = Factory.GetObjectsComparer(Settings, this); } public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) { + return BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differeneLocation => differeneLocation.Difference); + } + + public IEnumerable BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode listDifferenceTreeNode) + { + Debug.WriteLine($"{GetType().Name}.{nameof(BuildDifferenceTree)}: {type.Name}"); + + if (listDifferenceTreeNode is null) + { + throw new ArgumentNullException(nameof(listDifferenceTreeNode)); + } + if (!type.InheritsFrom(typeof(IEnumerable<>))) { throw new ArgumentException("Invalid type"); @@ -45,24 +59,33 @@ public override IEnumerable CalculateDifferences(Type type, object o var list1 = ((IEnumerable)obj1).ToList(); var list2 = ((IEnumerable)obj2).ToList(); + var listComparisonOptions = ListComparisonOptions.Default(); + Settings.ListComparisonOptionsAction?.Invoke(listDifferenceTreeNode, listComparisonOptions); + if (list1.Count != list2.Count) { if (!type.GetTypeInfo().IsArray) { - yield return new Difference("", list1.Count.ToString(), list2.Count.ToString(), - DifferenceTypes.NumberOfElementsMismatch); + yield return AddDifferenceToTree(listDifferenceTreeNode, "", list1.Count().ToString(), list2.Count().ToString(), DifferenceTypes.NumberOfElementsMismatch); } - yield break; + if (listComparisonOptions.UnequalListsComparisonEnabled == false) + { + yield break; + } } - for (var i = 0; i < list2.Count; i++) + var failrues = CalculateDifferences(list1, list2, listDifferenceTreeNode, listComparisonOptions); + + foreach (var failrue in failrues) { - foreach (var failure in _comparer.CalculateDifferences(list1[i], list2[i])) - { - yield return failure.InsertPath($"[{i}]"); - } + yield return failrue; } } + + public IEnumerable BuildDifferenceTree(T obj1, T obj2, IDifferenceTreeNode listDifferenceTreeNode) + { + return BuildDifferenceTree(((object)obj1 ?? obj2).GetType(), obj1, obj2, listDifferenceTreeNode); + } } } diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/ExpandoObjectComparer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/ExpandoObjectComparer.cs index ca8684f..f96ed27 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/ExpandoObjectComparer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/ExpandoObjectComparer.cs @@ -32,6 +32,12 @@ protected override IList GetProperties(ExpandoObject obj) return ((IDictionary) obj)?.Keys.ToList() ?? new List(); } + protected override bool TryGetMember(ExpandoObject obj, string propertyName, out MemberInfo value) + { + value = null; + return false; + } + protected override bool TryGetMemberValue(ExpandoObject obj, string propertyName, out object value) { if (obj != null) diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/GenericEnumerablesComparer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/GenericEnumerablesComparer.cs index 2c54bcf..fdac5ec 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/GenericEnumerablesComparer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/GenericEnumerablesComparer.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reflection; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; namespace ObjectsComparer @@ -16,6 +17,12 @@ public GenericEnumerablesComparer(ComparisonSettings settings, BaseComparer pare } public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) + { + return BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLocation => differenceLocation.Difference); + } + + public override IEnumerable BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode) { if (obj1 == null && obj2 == null) { @@ -44,7 +51,7 @@ public override IEnumerable CalculateDifferences(Type type, object o var enumerablesComparerType = typeof(EnumerablesComparer<>).MakeGenericType(elementType); var comparer = (IComparer)Activator.CreateInstance(enumerablesComparerType, Settings, this, Factory); - foreach (var difference in comparer.CalculateDifferences(type, obj1, obj2)) + foreach (var difference in comparer.TryBuildDifferenceTree(type, obj1, obj2, differenceTreeNode)) { yield return difference; } diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/HashSetsComparer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/HashSetsComparer.cs index 6775ab8..feff414 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/HashSetsComparer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/HashSetsComparer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; namespace ObjectsComparer @@ -14,6 +15,12 @@ public HashSetsComparer(ComparisonSettings settings, BaseComparer parentComparer } public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) + { + return BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLocation => differenceLocation.Difference); + } + + public override IEnumerable BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode) { if (obj1 == null && obj2 == null) { @@ -33,7 +40,7 @@ public override IEnumerable CalculateDifferences(Type type, object o var enumerablesComparerType = typeof(HashSetsComparer<>).MakeGenericType(elementType); var comparer = (IComparer)Activator.CreateInstance(enumerablesComparerType, Settings, this, Factory); - foreach (var difference in comparer.CalculateDifferences(type, obj1, obj2)) + foreach (var difference in comparer.TryBuildDifferenceTree(type, obj1, obj2, differenceTreeNode)) { yield return difference; } diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/HashSetsComparer~1.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/HashSetsComparer~1.cs index 0153066..da550da 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/HashSetsComparer~1.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/HashSetsComparer~1.cs @@ -1,19 +1,36 @@ using System; using System.Collections.Generic; using System.Linq; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; namespace ObjectsComparer { - internal class HashSetsComparer : AbstractComparer + internal class HashSetsComparer : AbstractComparer, IDifferenceTreeBuilder, IDifferenceTreeBuilder { public HashSetsComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) :base(settings, parentComparer, factory) { } + public IEnumerable BuildDifferenceTree(T obj1, T obj2, IDifferenceTreeNode differenceTreeNode) + { + return BuildDifferenceTree(typeof(T), obj1, obj2, differenceTreeNode); + } + public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) { + return BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLocation => differenceLocation.Difference); + } + + public IEnumerable BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode) + { + if (differenceTreeNode is null) + { + throw new ArgumentNullException(nameof(differenceTreeNode)); + } + if (!type.InheritsFrom(typeof(HashSet<>))) { throw new ArgumentException("Invalid type"); @@ -46,8 +63,9 @@ public override IEnumerable CalculateDifferences(Type type, object o { if (!hashSet2.Contains(element)) { - yield return new Difference("", valueComparer.ToString(element), string.Empty, - DifferenceTypes.MissedElementInSecondObject); + var differenceLocation = AddDifferenceToTree(differenceTreeNode, "", valueComparer.ToString(element), string.Empty, DifferenceTypes.MissedElementInSecondObject, element, null); + + yield return differenceLocation; } } @@ -55,11 +73,13 @@ public override IEnumerable CalculateDifferences(Type type, object o { if (!hashSet1.Contains(element)) { - yield return new Difference("", string.Empty, valueComparer.ToString(element), - DifferenceTypes.MissedElementInFirstObject); + var differenceLocation = AddDifferenceToTree(differenceTreeNode, "", string.Empty, valueComparer.ToString(element), DifferenceTypes.MissedElementInFirstObject, null, element); + + yield return differenceLocation; } } } + public bool IsMatch(Type type, object obj1, object obj2) { diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/MultidimensionalArrayComparer~1.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/MultidimensionalArrayComparer~1.cs index 0a84774..e009780 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/MultidimensionalArrayComparer~1.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/MultidimensionalArrayComparer~1.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; namespace ObjectsComparer { - internal class MultidimensionalArrayComparer : AbstractComparer + internal class MultidimensionalArrayComparer : AbstractComparer, IDifferenceTreeBuilder, IDifferenceTreeBuilder { private readonly IComparer _comparer; @@ -17,6 +18,22 @@ public MultidimensionalArrayComparer(ComparisonSettings settings, BaseComparer p public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) { + return BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLocation => differenceLocation.Difference); + } + + public IEnumerable BuildDifferenceTree(T obj1, T obj2, IDifferenceTreeNode differenceTreeNode) + { + return BuildDifferenceTree(typeof(T), obj1, obj2, differenceTreeNode); + } + + public IEnumerable BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode) + { + if (differenceTreeNode is null) + { + throw new ArgumentNullException(nameof(differenceTreeNode)); + } + if (!type.InheritsFrom(typeof(Array))) { throw new ArgumentException("Invalid type"); @@ -43,7 +60,8 @@ public override IEnumerable CalculateDifferences(Type type, object o if (array1.Rank != array2.Rank) { - yield return new Difference("Rank", array1.Rank.ToString(), array2.Rank.ToString()); + var differenceLocation = AddDifferenceToTree(differenceTreeNode, "Rank", array1.Rank.ToString(), array2.Rank.ToString()); + yield return differenceLocation; yield break; } @@ -57,7 +75,8 @@ public override IEnumerable CalculateDifferences(Type type, object o if (length1 != length2) { dimensionsFailure = true; - yield return new Difference($"Dimension{i}", length1.ToString(), length2.ToString()); + var differenceLocation = AddDifferenceToTree(differenceTreeNode, $"Dimension{i}", length1.ToString(), length2.ToString()); + yield return differenceLocation; } } @@ -70,9 +89,10 @@ public override IEnumerable CalculateDifferences(Type type, object o { var indecies = IndexToCoordinates(array1, i); - foreach (var failure in _comparer.CalculateDifferences((T)array1.GetValue(indecies), (T)array2.GetValue(indecies))) + foreach (var failure in _comparer.TryBuildDifferenceTree((T)array1.GetValue(indecies), (T)array2.GetValue(indecies), differenceTreeNode)) { - yield return failure.InsertPath($"[{string.Join(",", indecies)}]"); + failure.Difference.InsertPath($"[{string.Join(",", indecies)}]"); + yield return failure; } } } diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/MultidimensionalArraysComparer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/MultidimensionalArraysComparer.cs index d796c98..003bb4e 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/MultidimensionalArraysComparer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/MultidimensionalArraysComparer.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; namespace ObjectsComparer @@ -14,6 +16,12 @@ public MultidimensionalArraysComparer(ComparisonSettings settings, BaseComparer } public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) + { + return BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLocation => differenceLocation.Difference); + } + + public override IEnumerable BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode) { if (obj1 == null && obj2 == null) { @@ -24,7 +32,7 @@ public override IEnumerable CalculateDifferences(Type type, object o var enumerablesComparerType = typeof(MultidimensionalArrayComparer<>).MakeGenericType(typeInfo.GetElementType()); var comparer = (IComparer)Activator.CreateInstance(enumerablesComparerType, Settings, this, Factory); - foreach (var difference in comparer.CalculateDifferences(type, obj1, obj2)) + foreach (var difference in comparer.TryBuildDifferenceTree(type, obj1, obj2, differenceTreeNode)) { yield return difference; } diff --git a/ObjectsComparer/ObjectsComparer/CustomComparers/TypesComparer.cs b/ObjectsComparer/ObjectsComparer/CustomComparers/TypesComparer.cs index b733a9e..25f126f 100644 --- a/ObjectsComparer/ObjectsComparer/CustomComparers/TypesComparer.cs +++ b/ObjectsComparer/ObjectsComparer/CustomComparers/TypesComparer.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; +using ObjectsComparer.DifferenceTreeExtensions; using ObjectsComparer.Utils; namespace ObjectsComparer { - internal class TypesComparer : AbstractComparer, IComparerWithCondition + internal class TypesComparer : AbstractComparer, IComparerWithCondition, IDifferenceTreeBuilder { public TypesComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) @@ -15,6 +17,17 @@ public TypesComparer(ComparisonSettings settings, BaseComparer parentComparer, public override IEnumerable CalculateDifferences(Type type, object obj1, object obj2) { + return BuildDifferenceTree(type, obj1, obj2, DifferenceTreeNodeProvider.CreateImplicitRootNode(Settings)) + .Select(differenceLocation => differenceLocation.Difference); + } + + public IEnumerable BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode) + { + if (differenceTreeNode is null) + { + throw new ArgumentNullException(nameof(differenceTreeNode)); + } + if (obj1 == null && obj2 == null) { yield break; @@ -35,7 +48,7 @@ public override IEnumerable CalculateDifferences(Type type, object o if (type1Str != type2Str) { - yield return new Difference(string.Empty, type1Str, type2Str); + yield return AddDifferenceToTree(differenceTreeNode, string.Empty, type1Str, type2Str, DifferenceTypes.ValueMismatch, obj1, obj2); } } diff --git a/ObjectsComparer/ObjectsComparer/Difference.cs b/ObjectsComparer/ObjectsComparer/Difference.cs index 1341f0c..d38be54 100644 --- a/ObjectsComparer/ObjectsComparer/Difference.cs +++ b/ObjectsComparer/ObjectsComparer/Difference.cs @@ -8,7 +8,7 @@ public class Difference /// /// Path to the member. /// - public string MemberPath { get; } + public string MemberPath { get; private set; } /// /// Value in the first object, converted to string. @@ -25,20 +25,34 @@ public class Difference /// public DifferenceTypes DifferenceType { get; } + /// + /// The first object itself. + /// + public object RawValue1 { get; } + + /// + /// The second object itself. + /// + public object RawValue2 { get; } + /// /// Initializes a new instance of the class. /// /// Member Path. /// Value of the first object, converted to string. /// Value of the second object, converted to string. + /// The first object itself. + /// The second object itself. /// Type of the difference. public Difference(string memberPath, string value1, string value2, - DifferenceTypes differenceType = DifferenceTypes.ValueMismatch) + DifferenceTypes differenceType = DifferenceTypes.ValueMismatch, object rawValue1 = null, object rawValue2 = null) { MemberPath = memberPath; Value1 = value1; Value2 = value2; DifferenceType = differenceType; + RawValue1 = rawValue1; + RawValue2 = rawValue2; } /// @@ -52,11 +66,16 @@ public Difference InsertPath(string path) ? path + MemberPath : path + "." + MemberPath; - return new Difference( - newPath, - Value1, - Value2, - DifferenceType); + //This instance is probably already included in the difference tree, so I can't create a new one. + //return new Difference( + // newPath, + // Value1, + // Value2, + // DifferenceType); + + MemberPath = newPath; + + return this; } /// Returns a string that represents the current object. diff --git a/ObjectsComparer/ObjectsComparer/DifferenceOptions.cs b/ObjectsComparer/ObjectsComparer/DifferenceOptions.cs new file mode 100644 index 0000000..75744b2 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceOptions.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ObjectsComparer +{ + /// + /// Options for operation. + /// + public class DifferenceOptions + { + DifferenceOptions() + { + } + + /// + /// Default options. + /// + internal static DifferenceOptions Default() => new DifferenceOptions(); + + /// + /// Factory for instances. + /// + public Func DifferenceFactory { get; private set; } = null; + + /// + /// Factory for instances. + /// + /// + /// Null value is allowed here and means that a default behavior of creation of the difference is required.
+ /// First parameter: The args for the difference creation, see .
+ /// Returns: Transformed difference or the source difference itself. + /// + /// + public DifferenceOptions UseDifferenceFactory(Func factory) + { + DifferenceFactory = factory; + + return this; + } + + public DifferenceOptions IncludeRawValues(bool includeRawValues) + { + if (includeRawValues) + { + UseDifferenceFactory(args => new Difference( + args.DefaultDifference.MemberPath, + args.DefaultDifference.Value1, + args.DefaultDifference.Value2, + args.DefaultDifference.DifferenceType, + args.RawValue1, + args.RawValue2)); + } + else + { + UseDifferenceFactory(null); + } + + return this; + } + } +} diff --git a/ObjectsComparer/ObjectsComparer/DifferencePathOptions.cs b/ObjectsComparer/ObjectsComparer/DifferencePathOptions.cs new file mode 100644 index 0000000..1c071fb --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferencePathOptions.cs @@ -0,0 +1,35 @@ +using System; + +namespace ObjectsComparer +{ + /// + /// Configures the insertion into the difference path. + /// + public class DifferencePathOptions + { + DifferencePathOptions() + { + } + + /// + /// Default options. + /// + internal static DifferencePathOptions Default() => new DifferencePathOptions(); + + public Func InsertPathFactory { get; private set; } = null; + + /// + /// Factory for insertion into the property. + /// + /// + /// Null value is allowed here and means that a default behavior of the insertion into the difference path is required.
+ /// First parameter: The args for the path insertion, see .
+ /// Returns: Transformed root path element or not transformed root path element itself. + /// + public void UseInsertPathFactory(Func factory) + { + InsertPathFactory = factory; + } + + } +} diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/ComparerExtensions.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/ComparerExtensions.cs new file mode 100644 index 0000000..d24f401 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/ComparerExtensions.cs @@ -0,0 +1,85 @@ +using ObjectsComparer.DifferenceTreeExtensions; +using ObjectsComparer.Exceptions; +using ObjectsComparer.Utils; +using System; +using System.Collections.Generic; + +namespace ObjectsComparer +{ + public static class ComparerExtensions + { + /// + /// Calculates the difference tree. + /// + /// + /// Comparison process listener. If the argument is null the process is looking for all the differences at once.
+ /// First parameter: Current comparison context, see .
+ /// Second parameter type: Whether to look for another difference. If value = false the comparison process will be terminated immediately. + /// + /// Occurs if (and only if) the comparison process reaches the last member of the objects being compared. + /// The root node of the difference tree, see . + public static IDifferenceTreeNode CalculateDifferenceTree(this IComparer comparer, Type type, object obj1, object obj2, Func comparisonListener = null, Action differenceTreeCompleted = null) + { + if (comparer is null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + if (type is null) + { + throw new ArgumentNullException(nameof(type)); + } + + comparisonListener = comparisonListener ?? ((_) => true); + + //Anything but ImplicitDifferenceTreeNode. + var rootNode = DifferenceTreeNodeProvider.CreateNode(comparer.Settings, ancestor: null); + + var differenceLocationList = comparer.TryBuildDifferenceTree(type, obj1, obj2, rootNode); + + differenceLocationList.EnumerateConditional( + currentLocation => + { + return comparisonListener(new ComparisonContext(rootNode, currentLocation.Difference, currentLocation.TreeNode)); + }, + differenceTreeCompleted); + + return rootNode; + } + + /// + /// Calculates the difference tree. + /// + /// + /// Comparison process listener. If the argument is null the process is looking for all the differences at once.
+ /// First parameter: Current comparison context, see .
+ /// Second parameter type: Whether to look for another difference. If value = false the comparison process will be terminated immediately. + /// + /// Occurs if (and only if) the comparison process reaches the last member of the objects being compared. + /// The root node of the difference tree, see . + public static IDifferenceTreeNode CalculateDifferenceTree(this IComparer comparer, T obj1, T obj2, Func comparisonListener = null, Action differenceTreeCompleted = null) + { + if (comparer is null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + comparisonListener = comparisonListener ?? ((_) => true); + + //Anything but ImplicitDifferenceTreeNode. + var rootNode = DifferenceTreeNodeProvider.CreateNode(comparer.Settings, ancestor: null); + + var differenceLocationList = comparer.TryBuildDifferenceTree(obj1, obj2, rootNode); + + differenceLocationList.EnumerateConditional( + currentLocation => + { + return comparisonListener(new ComparisonContext(rootNode, currentLocation.Difference, currentLocation.TreeNode)); + }, + differenceTreeCompleted); + + return rootNode; + } + } +} + diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/ComparisonContext.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/ComparisonContext.cs new file mode 100644 index 0000000..71f664e --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/ComparisonContext.cs @@ -0,0 +1,24 @@ +using System; + +namespace ObjectsComparer +{ + /// + /// The context of the comparison in the process of creating the difference tree. For more info, see , or . + /// + public class ComparisonContext + { + public IDifferenceTreeNode RootNode { get; } + + public Difference Difference { get; } + + public IDifferenceTreeNode Node { get; } + + public ComparisonContext(IDifferenceTreeNode rootNode, Difference currentDifference, IDifferenceTreeNode currentNode = null) + { + RootNode = rootNode ?? throw new ArgumentNullException(nameof(rootNode)); + Difference = currentDifference ?? throw new ArgumentNullException(nameof(currentDifference)); + Node = currentNode; + } + } +} + diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceLocation.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceLocation.cs new file mode 100644 index 0000000..00da2a6 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceLocation.cs @@ -0,0 +1,23 @@ +using System; + +namespace ObjectsComparer.DifferenceTreeExtensions +{ + /// + /// The location of the difference in the difference tree. + /// + public class DifferenceLocation + { + public DifferenceLocation(Difference difference, IDifferenceTreeNode treeNode = null) + { + Difference = difference ?? throw new ArgumentNullException(nameof(difference)); + TreeNode = treeNode; + } + + public Difference Difference { get; } + + /// + /// Optional. Returns null, if no location is specified (probably by a comparer who does not implement ). + /// + public IDifferenceTreeNode TreeNode { get; } + } +} \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeBuilderExtensions.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeBuilderExtensions.cs new file mode 100644 index 0000000..ff6abc0 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeBuilderExtensions.cs @@ -0,0 +1,156 @@ +using ObjectsComparer.Exceptions; +using System; +using System.Linq; +using System.Collections.Generic; + +namespace ObjectsComparer.DifferenceTreeExtensions +{ + public static class DifferenceTreeBuilderExtensions + { + /// + /// If possible, creates a difference tree. + /// + /// + /// If is , it builds the difference tree. If not, it only builds the flat list of differences. + /// Intended for implementers. To avoid side effects, consumers should instead call extension method. + /// + /// The differences with their eventual location in the difference tree. + /// For more info see . + public static IEnumerable TryBuildDifferenceTree(this IComparer comparer, Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode) + { + _ = comparer ?? throw new ArgumentNullException(nameof(comparer)); + _ = type ?? throw new ArgumentNullException(nameof(type)); + _ = differenceTreeNode ?? throw new ArgumentNullException(nameof(differenceTreeNode)); + + if (comparer is IDifferenceTreeBuilder differenceTreeBuilder) + { + var differenceNodeLocationList = differenceTreeBuilder.BuildDifferenceTree(type, obj1, obj2, differenceTreeNode); + + foreach (var differenceNodeLocation in differenceNodeLocationList) + { + yield return differenceNodeLocation; + } + + yield break; + } + + ThrowDifferenceTreeBuilderNotImplemented(differenceTreeNode, comparer.Settings, comparer, nameof(IDifferenceTreeBuilder)); + + var differences = comparer.CalculateDifferences(type, obj1, obj2); + + foreach (var difference in differences) + { + yield return new DifferenceLocation(difference); + } + } + + /// + /// If possible, creates a difference tree. + /// + /// + /// If is , it builds the difference tree. If not, it only builds the flat list of differences. + /// Intended for implementers. To avoid side effects, consumers should call extension method instead. + /// + /// The differences with their eventual location in the difference tree. + /// For more info see . + public static IEnumerable TryBuildDifferenceTree(this IComparer comparer, T obj1, T obj2, IDifferenceTreeNode differenceTreeNode) + { + _ = comparer ?? throw new ArgumentNullException(nameof(comparer)); + _ = differenceTreeNode ?? throw new ArgumentNullException(nameof(differenceTreeNode)); + + if (comparer is IDifferenceTreeBuilder differenceTreeBuilder) + { + var differenceTreeNodeInfoList = differenceTreeBuilder.BuildDifferenceTree(obj1, obj2, differenceTreeNode); + + foreach (var differenceTreeNodeInfo in differenceTreeNodeInfoList) + { + yield return differenceTreeNodeInfo; + } + + yield break; + } + + ThrowDifferenceTreeBuilderNotImplemented(differenceTreeNode, comparer.Settings, comparer, $"{nameof(IDifferenceTreeBuilder)}<{typeof(T).FullName}>"); + + var differences = comparer.CalculateDifferences(obj1, obj2); + + foreach (var difference in differences) + { + yield return new DifferenceLocation(difference); + } + } + + /// + /// See . + /// + static bool HasDifferenceTreeImplicitRoot(IDifferenceTreeNode differenceTreeNode) + { + if (differenceTreeNode is null) + { + throw new ArgumentNullException(nameof(differenceTreeNode)); + } + + do + { + if (differenceTreeNode.Ancestor == null && differenceTreeNode is ImplicitDifferenceTreeNode) + { + return true; + } + + differenceTreeNode = differenceTreeNode.Ancestor; + + } while (differenceTreeNode != null); + + return false; + } + + internal static void ThrowDifferenceTreeBuilderNotImplemented(IDifferenceTreeNode differenceTreeNode, ComparisonSettings comparisonSettings, object comparer, string unImplementedInterface) + { + _ = differenceTreeNode ?? throw new ArgumentNullException(nameof(differenceTreeNode)); + _ = comparisonSettings ?? throw new ArgumentNullException(nameof(comparisonSettings)); + + var options = DifferenceTreeOptions.Default(); + comparisonSettings.DifferenceTreeOptionsAction?.Invoke(null, options); + + if (options.ThrowDifferenceTreeBuilderNotImplementedEnabled == false) + { + return; + } + + if (comparisonSettings.DifferenceTreeOptionsAction != null) + { + var message = $"Because the difference tree has been explicitly configured, the {comparer.GetType().FullName} must implement {unImplementedInterface} interface " + + "or throwing the DifferenceTreeBuilderNotImplementedException must be disabled."; + throw new DifferenceTreeBuilderNotImplementedException(message); + } + + if (comparisonSettings.ListComparisonOptionsAction != null) + { + var message = $"Because the list comparison has been explicitly configured, the {comparer.GetType().FullName} must implement {unImplementedInterface} interface " + + "or throwing the DifferenceTreeBuilderNotImplementedException must be disabled."; + throw new DifferenceTreeBuilderNotImplementedException(message); + } + + if (comparisonSettings.DifferenceOptionsAction != null) + { + var message = $"Because the difference has been explicitly configured, the {comparer.GetType().FullName} must implement {unImplementedInterface} interface " + + "or throwing the DifferenceTreeBuilderNotImplementedException must be disabled."; + throw new DifferenceTreeBuilderNotImplementedException(message); + } + + if (comparisonSettings.DifferencePathOptionsAction != null) + { + var message = $"Because the difference path has been explicitly configured, the {comparer.GetType().FullName} must implement {unImplementedInterface} interface " + + "or throwing the DifferenceTreeBuilderNotImplementedException must be disabled."; + throw new DifferenceTreeBuilderNotImplementedException(message); + } + + if (HasDifferenceTreeImplicitRoot(differenceTreeNode) == false) + { + var message = $"Because the difference tree has been explicitly passed, the {comparer.GetType().FullName} must implement {unImplementedInterface} interface " + + "or throwing the DifferenceTreeBuilderNotImplementedException must be disabled."; + throw new DifferenceTreeBuilderNotImplementedException(message); + } + } + } +} \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNode.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNode.cs new file mode 100644 index 0000000..fd7bfb3 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNode.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; + +namespace ObjectsComparer +{ + /// + /// Default implementation of . + /// + public sealed class DifferenceTreeNode : DifferenceTreeNodeBase + { + public DifferenceTreeNode(IDifferenceTreeNodeMember member = null, IDifferenceTreeNode ancestor = null) : base(member, ancestor) + { + + } + } +} \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNodeBase.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNodeBase.cs new file mode 100644 index 0000000..db104ac --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNodeBase.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ObjectsComparer +{ + /// + /// Base class for implementors. + /// + public abstract class DifferenceTreeNodeBase : IDifferenceTreeNode + { + readonly List _descendants = new List(); + + readonly List _differences = new List(); + + public DifferenceTreeNodeBase(IDifferenceTreeNodeMember member = null, IDifferenceTreeNode ancestor = null) + { + ancestor?.AddDescendant(this); + Member = member; + } + + IDifferenceTreeNode _ancestor; + + public virtual IDifferenceTreeNode Ancestor + { + get + { + return _ancestor; + } + set + { + if (_ancestor != null) + { + throw new InvalidOperationException("The ancestor already exists."); + } + + _ancestor = value; + } + } + + public IEnumerable Descendants => _descendants.AsReadOnly(); + + public IEnumerable Differences => _differences.AsReadOnly(); + + public IDifferenceTreeNodeMember Member { get; } + + public void AddDescendant(IDifferenceTreeNode descendant) + { + if (descendant is null) + { + throw new ArgumentNullException(nameof(descendant)); + } + + _descendants.Add(descendant); + descendant.Ancestor = this; + } + + public virtual void AddDifference(Difference difference) + { + if (difference is null) + { + throw new ArgumentNullException(nameof(difference)); + } + + _differences.Add(difference); + } + + public IEnumerable GetDifferences(bool recursive) + { + foreach (var difference in _differences) + { + yield return difference; + } + + if (recursive) + { + foreach (var descendant in _descendants) + { + var differences = descendant.GetDifferences(true); + foreach (var difference in differences) + { + yield return difference; + } + } + } + } + + public bool HasDifferences(bool recursive) + { + return GetDifferences(recursive).Any(); + } + + public void Shrink() + { + List removeDescendants = new List(); + + _descendants.ForEach(descendantNode => + { + descendantNode.Shrink(); + + if (descendantNode.HasDifferences(true) == false) + { + removeDescendants.Add(descendantNode); + } + }); + + _descendants.RemoveAll(descendantTreeNode => removeDescendants.Contains(descendantTreeNode )); + } + } +} diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNodeMember.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNodeMember.cs new file mode 100644 index 0000000..aec9bff --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNodeMember.cs @@ -0,0 +1,18 @@ +using System; +using System.Reflection; + +namespace ObjectsComparer +{ + public class DifferenceTreeNodeMember : IDifferenceTreeNodeMember + { + public DifferenceTreeNodeMember(MemberInfo info = null, string name = null) + { + Info = info; + Name = name; + } + + public MemberInfo Info { get; } + + public string Name { get; } + } +} \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNodeProvider.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNodeProvider.cs new file mode 100644 index 0000000..d25664f --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeNodeProvider.cs @@ -0,0 +1,76 @@ +using System; +using System.Reflection; + +namespace ObjectsComparer +{ + public static class DifferenceTreeNodeProvider + { + public static IDifferenceTreeNode CreateRootNode() + { + return new DifferenceTreeNode(new DifferenceTreeNodeMember()); + } + + /// + /// Returns the root of the difference tree for cases where the consumer does not explicitly, directly or indirectly request the difference tree, i.e the difference tree is created only as an auxiliary. + /// + public static IDifferenceTreeNode CreateImplicitRootNode(ComparisonSettings comparisonSettings) + { + _ = comparisonSettings ?? throw new ArgumentNullException(nameof(comparisonSettings)); + + return new ImplicitDifferenceTreeNode(new DifferenceTreeNodeMember()); + } + + public static IDifferenceTreeNode CreateNode(ComparisonSettings comparisonSettings, IDifferenceTreeNode ancestor) + { + return CreateNode(comparisonSettings, new DifferenceTreeNodeMember(), ancestor); + } + + public static IDifferenceTreeNode CreateNode(ComparisonSettings comparisonSettings, IDifferenceTreeNode ancestor, MemberInfo memberInfo) + { + return CreateNode(comparisonSettings, new DifferenceTreeNodeMember(memberInfo, memberInfo?.Name), ancestor); + } + + public static IDifferenceTreeNode CreateNode(ComparisonSettings comparisonSettings, IDifferenceTreeNode ancestor, string memberName) + { + return CreateNode(comparisonSettings, new DifferenceTreeNodeMember(name: memberName), ancestor); + } + + public static IDifferenceTreeNode CreateNode(ComparisonSettings comparisonSettings, IDifferenceTreeNode ancestor, MemberInfo memberInfo, string memberName) + { + return CreateNode(comparisonSettings, new DifferenceTreeNodeMember(memberInfo, memberName), ancestor); + } + + public static IDifferenceTreeNode CreateNode(ComparisonSettings comparisonSettings, IDifferenceTreeNodeMember differenceTreeNodeMember, IDifferenceTreeNode ancestor) + { + _ = comparisonSettings ?? throw new ArgumentNullException(nameof(comparisonSettings)); + _ = differenceTreeNodeMember ?? throw new ArgumentNullException(nameof(differenceTreeNodeMember)); + //_ = ancestor ?? throw new ArgumentNullException(nameof(ancestor)); + + DifferenceTreeOptions options = DifferenceTreeOptions.Default(); + comparisonSettings.DifferenceTreeOptionsAction?.Invoke(ancestor, options); + + if (options.DifferenceTreeNodeMemberFactory != null) + { + var customDifferenceTreeNodeMember = options.DifferenceTreeNodeMemberFactory.Invoke(differenceTreeNodeMember); + differenceTreeNodeMember = customDifferenceTreeNodeMember ?? throw new InvalidOperationException("Difference tree node member factory returned null member."); + } + + if (options.DifferenceTreeNodeFactory != null) + { + var customDifferenceTreeNode = options.DifferenceTreeNodeFactory(differenceTreeNodeMember); + + if (customDifferenceTreeNode != null) + { + return customDifferenceTreeNode; + } + + if (customDifferenceTreeNode == null) + { + throw new InvalidOperationException("Difference tree node factory returned null node."); + } + } + + return new DifferenceTreeNode(differenceTreeNodeMember, ancestor); + } + } +} diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeOptions.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeOptions.cs new file mode 100644 index 0000000..9b03fa6 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/DifferenceTreeOptions.cs @@ -0,0 +1,57 @@ +using System; +using ObjectsComparer.Exceptions; +using ObjectsComparer.DifferenceTreeExtensions; + +namespace ObjectsComparer +{ + public class DifferenceTreeOptions + { + DifferenceTreeOptions() + { + } + + internal static DifferenceTreeOptions Default() + { + return new DifferenceTreeOptions(); + } + + internal Func DifferenceTreeNodeFactory { get; private set; } + + internal Func DifferenceTreeNodeMemberFactory { get; private set; } + + /// + /// Factory for instances. + /// + /// + /// First parameter: The member the tree node is created for. It can be replaced by a custom instance. + /// + public void UseDifferenceTreeNodeFactory(Func factory) + { + DifferenceTreeNodeFactory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + /// + /// Factory for instances. + /// + /// + /// First parameter: Default member, it can be used as a falback from the delegate. + /// + public void UseDifferenceTreeNodeMemberFactory(Func factory) + { + DifferenceTreeNodeMemberFactory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + public bool ThrowDifferenceTreeBuilderNotImplementedEnabled { get; private set; } = true; + + /// + /// Whether to throw the when the user requires the difference tree but has a comparer that does not implement or . + /// Default = true. + /// + public DifferenceTreeOptions ThrowDifferenceTreeBuilderNotImplemented(bool value) + { + ThrowDifferenceTreeBuilderNotImplementedEnabled = value; + + return this; + } + } +} diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeBuilder.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeBuilder.cs new file mode 100644 index 0000000..5bfd76b --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeBuilder.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace ObjectsComparer.DifferenceTreeExtensions +{ + /// + /// Builds the difference tree. + /// + public interface IDifferenceTreeBuilder + { + /// + /// Finds the difference, adds it to the difference tree and returns it, including its location. + /// + /// Intended for implementers. To avoid side effects, consumers should call extension method instead. + /// The starting point in the tree from which the differences should be built. + /// The list of the differences and their location in the difference tree. + IEnumerable BuildDifferenceTree(Type type, object obj1, object obj2, IDifferenceTreeNode differenceTreeNode); + } +} \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeBuilder~1.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeBuilder~1.cs new file mode 100644 index 0000000..61f5da9 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeBuilder~1.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace ObjectsComparer.DifferenceTreeExtensions +{ + /// + /// Builds the difference tree. + /// + public interface IDifferenceTreeBuilder + { + /// + /// Finds the difference, adds it to the difference tree and returns it, including its location. + /// + /// Intended for implementers. To avoid side effects, consumers should call extension method instead. + /// The starting point in the tree from which the differences should be built. + /// The list of the differences and their location in the difference tree. + IEnumerable BuildDifferenceTree(T obj1, T obj2, IDifferenceTreeNode differenceTreeNode); + } +} diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeNode.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeNode.cs new file mode 100644 index 0000000..37d36ab --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeNode.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; + +namespace ObjectsComparer +{ + /// + /// Node in the difference tree. + /// + /// + /// Each instance of the node wraps compared , which is typically a property. Each node has its ancestor and descendants the same way as its compared has its ancestor and descendant members.
+ /// Once the calculation of the difference tree is finished (completed or uncompleted), it is possible to traverse the difference tree and see differences at particular members.
+ /// For more about calculation of the difference tree see or . + ///
+ public interface IDifferenceTreeNode + { + /// + /// Ancestor node. + /// + IDifferenceTreeNode Ancestor { get; set; } + + /// + /// Children nodes. + /// + IEnumerable Descendants { get; } + + /// + /// A list of differences directly related to the node. + /// + IEnumerable Differences { get; } + + /// + /// Compared member, for more info see . + /// It should be null for the root node (the starting point of the comparison) and for the list element node. A list element node never has a member, but it has an ancestor node which is the list and that list has its member. + /// + IDifferenceTreeNodeMember Member { get; } + + /// + /// Adds descendant to the node. + /// + /// + void AddDescendant(IDifferenceTreeNode descendant); + + /// + /// Adds the difference to the node. + /// + /// + void AddDifference(Difference difference); + + /// + /// Returns differences directly or indirectly related to the node. + /// + /// If value is true, it also looks for in . + IEnumerable GetDifferences(bool recursive = true); + + /// + /// Whether there are differences directly or indirectly related to the node. + /// + /// If value is true, it also looks for in . + bool HasDifferences(bool recursive); + + /// + /// Removes all which have no directly or indirectly in their . + /// + void Shrink(); + } +} diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeNodeMember.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeNodeMember.cs new file mode 100644 index 0000000..22d4609 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/IDifferenceTreeNodeMember.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +namespace ObjectsComparer +{ + /// + /// The member in the comparison process, usually a property. + /// + public interface IDifferenceTreeNodeMember + { + /// + /// Member. It should never be empty. + /// + string Name { get; } + + /// + /// Member. May be null for dynamic properties unknown at compile time. + /// + MemberInfo Info { get; } + } +} \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/DifferenceTree/ImplicitDifferenceTreeNode.cs b/ObjectsComparer/ObjectsComparer/DifferenceTree/ImplicitDifferenceTreeNode.cs new file mode 100644 index 0000000..8e05256 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/DifferenceTree/ImplicitDifferenceTreeNode.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; + +namespace ObjectsComparer +{ + /// + /// The root of the difference tree for cases where the consumer does not explicitly, directly or indirectly request the difference tree, i.e the difference tree is created only as an auxiliary. + /// + internal class ImplicitDifferenceTreeNode : DifferenceTreeNodeBase + { + public ImplicitDifferenceTreeNode(IDifferenceTreeNodeMember member = null, IDifferenceTreeNode ancestor = null) : base(member, ancestor) + { + } + + public override void AddDifference(Difference difference) + { + } + } +} diff --git a/ObjectsComparer/ObjectsComparer/Exceptions/DifferenceTreeBuilderNotImplementedException.cs b/ObjectsComparer/ObjectsComparer/Exceptions/DifferenceTreeBuilderNotImplementedException.cs new file mode 100644 index 0000000..70d6686 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/Exceptions/DifferenceTreeBuilderNotImplementedException.cs @@ -0,0 +1,16 @@ +using System; +using ObjectsComparer.DifferenceTreeExtensions; + +namespace ObjectsComparer.Exceptions +{ + /// + /// Depending on the configuration or actual state of the comparison process, this exception may be thrown when a user defined comparer does not implement or . + /// To prevent this exception from being thrown, see operation. + /// + public class DifferenceTreeBuilderNotImplementedException : NotImplementedException + { + internal DifferenceTreeBuilderNotImplementedException(string message) : base(message) + { + } + } +} diff --git a/ObjectsComparer/ObjectsComparer/Exceptions/ElementKeyNotFoundException.cs b/ObjectsComparer/ObjectsComparer/Exceptions/ElementKeyNotFoundException.cs new file mode 100644 index 0000000..6708e57 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/Exceptions/ElementKeyNotFoundException.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ObjectsComparer.Exceptions +{ + /// + /// Depending on the configuration, the exception is thrown if the key provider does not supply the key for list element . For more information see and . + /// + public class ElementKeyNotFoundException : Exception + { + const string ElementKeyNotFoundExceptionMsg = "Element key not found."; + + /// + /// See . + /// + /// An element that is missing a key. + /// + internal ElementKeyNotFoundException(object keylessElement, IDifferenceTreeNode keylessElementDifferenceTreeNode, string message = ElementKeyNotFoundExceptionMsg) : base(message) + { + KeylessElement = keylessElement ?? throw new ArgumentNullException(nameof(keylessElement)); + KeylessElementDifferenceTreeNode = keylessElementDifferenceTreeNode ?? throw new ArgumentNullException(nameof(keylessElementDifferenceTreeNode)); + } + + /// + /// An element that is missing a key. + /// + public object KeylessElement { get; } + + /// + /// The current in which the exception occurred. + /// + public IDifferenceTreeNode KeylessElementDifferenceTreeNode { get; } + } +} diff --git a/ObjectsComparer/ObjectsComparer/FormatListElementKeyArgs.cs b/ObjectsComparer/ObjectsComparer/FormatListElementKeyArgs.cs new file mode 100644 index 0000000..c373465 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/FormatListElementKeyArgs.cs @@ -0,0 +1,23 @@ +using System; + +namespace ObjectsComparer +{ + /// + /// Useful arguments for list element key formatter. See . + /// + public class FormatListElementKeyArgs + { + internal FormatListElementKeyArgs(int elementIndex, object elementKey, object element) + { + ElementIndex = elementIndex; + ElementKey = elementKey ?? throw new ArgumentNullException(nameof(elementKey)); + Element = element ?? throw new ArgumentNullException(nameof(element)); + } + + public int ElementIndex { get; } + + public object ElementKey { get; } + + public object Element { get; } + } +} diff --git a/ObjectsComparer/ObjectsComparer/FormatNullElementIdentifierArgs.cs b/ObjectsComparer/ObjectsComparer/FormatNullElementIdentifierArgs.cs new file mode 100644 index 0000000..2dae19f --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/FormatNullElementIdentifierArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace ObjectsComparer +{ + /// + /// Useful arguments for null element identifier formatter. See . + /// + public class FormatNullElementIdentifierArgs + { + internal FormatNullElementIdentifierArgs(int elementIndex) + { + ElementIndex = elementIndex; + } + + public int ElementIndex { get; } + } +} diff --git a/ObjectsComparer/ObjectsComparer/InsertPathFactoryArgs.cs b/ObjectsComparer/ObjectsComparer/InsertPathFactoryArgs.cs new file mode 100644 index 0000000..918008a --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/InsertPathFactoryArgs.cs @@ -0,0 +1,24 @@ +namespace ObjectsComparer +{ + /// + /// Arguments for . + /// + public class InsertPathFactoryArgs + { + public InsertPathFactoryArgs(string defaultRootElementPath, IDifferenceTreeNode childNode) + { + DefaultRootElementPath = defaultRootElementPath; + ChildNode = childNode; + } + + /// + /// The path that will be inserted to the property by default. + /// + public string DefaultRootElementPath { get; } + + /// + /// The property to which the path is inserted. + /// + public IDifferenceTreeNode ChildNode { get; } + } +} diff --git a/ObjectsComparer/ObjectsComparer/ListComparisonOptions.cs b/ObjectsComparer/ObjectsComparer/ListComparisonOptions.cs new file mode 100644 index 0000000..d6d1d0f --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/ListComparisonOptions.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace ObjectsComparer +{ + /// + /// Configures list comparison behavior. + /// + public class ListComparisonOptions + { + ListComparisonOptions() + { + } + + /// + /// See . + /// + public bool UnequalListsComparisonEnabled { get; private set; } = false; + + /// + /// Whether to compare elements of the lists even if their number differs. Regardless of the , if lists are unequal, the difference of type will always be logged. Default value = false - unequal lists will not be compared. + /// + public ListComparisonOptions CompareUnequalLists(bool value) + { + UnequalListsComparisonEnabled = value; + + return this; + } + + /// + /// Default options. + /// + /// + internal static ListComparisonOptions Default() => new ListComparisonOptions(); + + /// + /// Compares list elements by index. Default behavior. + /// + public ListComparisonOptions CompareElementsByIndex() + { + KeyOptionsAction = null; + + return this; + } + + public Action KeyOptionsAction { get; private set; } + + /// + /// Compares list elements by key using element key provider. + /// + public ListComparisonOptions CompareElementsByKey() + { + return CompareElementsByKey(options => { }); + } + + /// + /// Compares list elements by key. + /// + /// List element comparison options + public ListComparisonOptions CompareElementsByKey(Action keyOptions) + { + if (keyOptions is null) + { + throw new ArgumentNullException(nameof(keyOptions)); + } + + KeyOptionsAction = keyOptions; + + return this; + } + + /// + /// See . + /// + internal ListElementSearchMode ElementSearchMode => KeyOptionsAction == null ? ListElementSearchMode.Index : ListElementSearchMode.Key; + } +} diff --git a/ObjectsComparer/ObjectsComparer/ListElementComparisonByKeyOptions.cs b/ObjectsComparer/ObjectsComparer/ListElementComparisonByKeyOptions.cs new file mode 100644 index 0000000..3fba038 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/ListElementComparisonByKeyOptions.cs @@ -0,0 +1,301 @@ +using System; +using System.Linq; +using System.Reflection; +using ObjectsComparer.Exceptions; +using ObjectsComparer; +using ObjectsComparer.Utils; + +namespace ObjectsComparer +{ + /// + /// Configures the behavior of list elements if list elements are to be compared by key. + /// + public class ListElementComparisonByKeyOptions + { + /// + /// Default element identifier template for element that refers to null value. See for more info. + /// + public const string DefaultNullElementIdentifierTemplate = "NullAtIdx={0}"; + + /// + /// Max. length of the formatted key of the element. See . + /// + const int FormattedElementKeyMaxLength = 50; + + /// + /// Max. length of the identifier of the element that refers to null value. See . + /// + const int NullElementIdentifierMaxLength = 20; + + /// + /// Element keys supported by . + /// + static readonly string[] DefaultElementKeys = new string[] { "Id", "Key", "Name"}; + + /// + /// + /// + const bool DefaultElementKeyCaseSensitivity = false; + + ListElementComparisonByKeyOptions() + { + DefaultElementKeyProviderAction = args => + { + if (TryGetKeyValueFromElement(args.Element, out var keyValue2)) + { + return keyValue2; + } + + if (TryGetPropertyValue(args.Element, caseSensitive: DefaultElementKeyCaseSensitivity, out var keyValue, DefaultElementKeys)) + { + return keyValue; + } + + return null; + + }; + + UseKey(DefaultElementKeyProviderAction); + } + + /// + /// See . + /// + Func ElementKeyFormatter { get; set; } + + /// + /// See . + /// + Func NullElementIdentifierFormatter { get; set; } + + /// + /// See . + /// + internal bool ThrowKeyNotFoundEnabled { get; set; } = true; + + /// + /// If value = false and element key will not be found, the element will be excluded from comparison and no difference will be added except for possible difference. + /// If value = true and element key will not be found, an exception of type will be thrown. + /// Default value = true. + /// + /// + /// + public ListElementComparisonByKeyOptions ThrowKeyNotFound(bool value) + { + ThrowKeyNotFoundEnabled = value; + + return this; + } + + /// + /// See . + /// + internal Func ElementKeyProviderAction { get; private set; } = null; + + /// + /// Default list element key provider. If the element implements , the provider returns the element itself. + /// Otherwise, if the element contains one of the properties "Id", "Key", "Name", the provider returns first of them, in that order, even it will be null. + /// Otherwise the provider returns null. + /// + public Func DefaultElementKeyProviderAction { get; private set; } = null; + + internal static ListElementComparisonByKeyOptions Default() => new ListElementComparisonByKeyOptions(); + + /// + /// Key identification. It attempts to find the key using the property specified by the parameter. + /// + public ListElementComparisonByKeyOptions UseKey(string key, bool caseSensitive = false) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentException($"'{nameof(key)}' cannot be null or whitespace.", nameof(key)); + } + + return UseKey(new string[] { key }, caseSensitive); + } + + /// + /// Key identification. It attempts to find the key using one of the public properties specified by the parameter, in the specified order. + /// + public ListElementComparisonByKeyOptions UseKey(string[] keys, bool caseSensitive = false) + { + if (keys is null) + { + throw new ArgumentNullException(nameof(keys)); + } + + if (keys.Any() == false) + { + throw new ArgumentException("At least one key is required.", nameof(keys)); + } + + if (keys.Any(key => string.IsNullOrWhiteSpace(key))) + { + throw new ArgumentException($"'{nameof(keys)}' cannot contain null or whitespace.", nameof(keys)); + } + + return UseKey(args => + { + TryGetPropertyValue(args.Element, caseSensitive, out var propertyValue, keys); + return propertyValue; + }); + } + + /// + /// Key identification. It attempts to find the key using the parameter. + /// + /// First parameter: The element whose key is required, see .
+ /// Return value: The element's key. + public ListElementComparisonByKeyOptions UseKey(Func keyProvider) + { + if (keyProvider is null) + { + throw new ArgumentNullException(nameof(keyProvider)); + } + + ElementKeyProviderAction = keyProvider; + + return this; + } + + /// + /// The out parameter returns first property value of the from properties defined by , in specified order. Value can be null. + /// If no property is found in the object, false is returned by operation. + /// + bool TryGetPropertyValue(object instance, bool caseSensitive, out object value, params string[] properties) + { + if (instance != null) + { + BindingFlags bindingAttr = BindingFlags.Public | BindingFlags.Instance; + if (caseSensitive == false) + { + bindingAttr |= BindingFlags.IgnoreCase; + } + + foreach (var key in properties) + { + var property = instance.GetType().GetTypeInfo().GetProperty(key, bindingAttr); + if (property != null) + { + value = property.GetValue(instance); + return true; + } + } + } + + value = null; + return false; + } + + /// + /// If is returns , otherwise returns null. + /// + /// + /// + /// + internal static bool TryGetKeyValueFromElement(object element, out object keyValue) + { + var elementType = element.GetType(); + + if (elementType.InheritsFrom(typeof(System.Collections.Generic.KeyValuePair<,>))) + { + keyValue = elementType.GetTypeInfo().GetProperty("Key").GetValue(element); + return TryGetKeyValueFromElement(keyValue, out keyValue); + } + + if (elementType.InheritsFrom(typeof(IEquatable<>))) + { + keyValue = element; + return true; + } + + //if (elementType.InheritsFrom(Nullable.GetUnderlyingType(elementType))) + //{ + // keyValue = element; + // return true; + //} + + keyValue = null; + return false; + } + + /// + /// Returns optional formatted or unformatted . See + /// + internal string GetFormattedElementKey(FormatListElementKeyArgs formatElementKeyArgs) + { + if (formatElementKeyArgs is null) + { + throw new ArgumentNullException(nameof(formatElementKeyArgs)); + } + + var formattedKey = ElementKeyFormatter?.Invoke(formatElementKeyArgs); + + if (string.IsNullOrWhiteSpace(formattedKey)) + { + formattedKey = formatElementKeyArgs.ElementKey.ToString(); + } + + return formattedKey.Left(FormattedElementKeyMaxLength); + } + + /// + /// Returns element identifier for element that referes to null. See . + /// + /// Element index. + internal string GetNullElementIdentifier(FormatNullElementIdentifierArgs args) + { + if (args is null) + { + throw new ArgumentNullException(nameof(args)); + } + + var elementIdentifier = NullElementIdentifierFormatter?.Invoke(args); + + if (string.IsNullOrWhiteSpace(elementIdentifier)) + { + elementIdentifier = string.Format(DefaultNullElementIdentifierTemplate, args.ElementIndex); + } + + return elementIdentifier.Left(NullElementIdentifierMaxLength); + } + + /// + /// To avoid possible confusion of the element key with the element index, the element key can be formatted with any text.
+ /// For example, element key with value = 1 can be formatted as "Id=1". + /// The formatted element key is then used as part of the property, e.g. "Addresses[Id=1]" instead of "Addresses[1]".
+ /// By default the element key is not formatted. + ///
+ public ListElementComparisonByKeyOptions FormatElementKey(Func formatter) + { + if (formatter is null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + ElementKeyFormatter = formatter; + + return this; + } + + /// + /// Formats the element identifier if it refers to null. Formatted identifier is then used as part of the property.
+ /// By default, template will be used to format the identifier. + ///
+ /// + /// First parameter: Element index.
+ /// Second parameter type: Formatted identifier. + /// + public ListElementComparisonByKeyOptions FormatNullElementIdentifier(Func formatter) + { + if (formatter is null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + NullElementIdentifierFormatter = formatter; + + return this; + } + } +} diff --git a/ObjectsComparer/ObjectsComparer/ListElementKeyProviderArgs.cs b/ObjectsComparer/ObjectsComparer/ListElementKeyProviderArgs.cs new file mode 100644 index 0000000..ff0e39f --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/ListElementKeyProviderArgs.cs @@ -0,0 +1,21 @@ +using System; + +namespace ObjectsComparer +{ + public class ListElementKeyProviderArgs + { + /// + /// + /// + /// The element whose key is required. + public ListElementKeyProviderArgs(object element) + { + Element = element ?? throw new ArgumentNullException(nameof(element)); + } + + /// + /// The element whose key is required. + /// + public object Element { get; } + } +} diff --git a/ObjectsComparer/ObjectsComparer/ListElementSearchingMode.cs b/ObjectsComparer/ObjectsComparer/ListElementSearchingMode.cs new file mode 100644 index 0000000..a39e0b2 --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/ListElementSearchingMode.cs @@ -0,0 +1,18 @@ +namespace ObjectsComparer +{ + /// + /// List element search mode. + /// + internal enum ListElementSearchMode + { + /// + /// The elements will be searched according to their index. + /// + Index, + + /// + /// The elements will be searched according to their key. + /// + Key + } +} diff --git a/ObjectsComparer/ObjectsComparer/Utils/EnumerableExtensions.cs b/ObjectsComparer/ObjectsComparer/Utils/EnumerableExtensions.cs new file mode 100644 index 0000000..6b51ead --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/Utils/EnumerableExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; + +namespace ObjectsComparer.Utils +{ + internal static class EnumerableExtensions + { + internal static void EnumerateConditional(this IEnumerable enumerable, Func findNextElement = null, Action enumerationCompleted = null) + { + _ = enumerable ?? throw new ArgumentNullException(nameof(enumerable)); + + var enumerator = enumerable.GetEnumerator(); + var enumerationTerminated = false; + + while (enumerator.MoveNext()) + { + if (findNextElement?.Invoke(enumerator.Current) == false) + { + enumerationTerminated = true; + break; + } + } + + if (enumerationTerminated == false) + { + enumerationCompleted?.Invoke(); + } + } + } +} \ No newline at end of file diff --git a/ObjectsComparer/ObjectsComparer/Utils/StringExtensions.cs b/ObjectsComparer/ObjectsComparer/Utils/StringExtensions.cs new file mode 100644 index 0000000..9f61cba --- /dev/null +++ b/ObjectsComparer/ObjectsComparer/Utils/StringExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ObjectsComparer.Utils +{ + internal static class StringExtensions + { + /// + /// Returns the left part of with the of characters. + /// + /// + /// + /// + public static string Left(this string value, int length) + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + return value.Substring(0, value.Length > length ? length : value.Length); + } + } +}