ValeraT1982 / ObjectsComparer

C# Framework provides mechanism to compare complex objects, allows to override comparison rules for specific properties and types.
MIT License
352 stars 86 forks source link

List of sub objects. #19

Closed kenperregaux closed 3 years ago

kenperregaux commented 4 years ago

I am investigating this package and and was very excited about it. However, I am running into a bit of a road block and would love some help. Below are the classes and test data I am using to test with and as you can see the main class has a list of a sub class. However, the only differences I get are the name difference if there is a difference in length of lists...I was hoping it would return the name difference, the difference in Val1 of the second item in the list and the complete additional object from the list in the second main object. Any help on this would be awesome, thanks in advance. The funny thing is if I comment out the 3rd element in the list of test2 then I do get the difference of the 2nd element in the list.

// Classes internal class TestSub { public string Val1 { get; set; } public int Val2 { get; set; } }; internal class TestMain { public string Name { get; set; } public List TestSubs { get; set; } };

// Test data and differences run var test1 = new TestMain { Name = "Ken", TestSubs = new List { new TestSub { Val1 = "val1", Val2 = 1 }, new TestSub { Val1 = "val2", Val2 = 2 } } }; var test2 = new TestMain { Name = "KenP", TestSubs = new List { new TestSub { Val1 = "val1", Val2 = 1 }, new TestSub { Val1 = "val22", Val2 = 2 }, new TestSub { Val1 = "val3", Val2 = 3 } } }; var testComparer = new ObjectsComparer.Comparer(); IEnumerable testDiffs; var t = testComparer.Compare(test1, test2, out testDiffs);

ValeraT1982 commented 4 years ago

@kenperregaux behavior you described is correct. Comparer compare individual elements of the list only if lists have the same number of elements.

You can write your custom comparer to compare elements. This example compare elements based on ids.

More details why it's not implemented yet here

I'm going to implement some custom list comparers in the next version.

kenperregaux commented 4 years ago

Thanks for the quick response. Will take a look at the example!

kenperregaux commented 4 years ago

I am running your test code right now and have a couple questions.

  1. I set break points in the comparer inside CalculateDifferences but it never gets hit. I tried to do this because my custom comparer that I wrote doesn't seem to be getting called or is not working correctly and I'd like to find out why.
  2. What I would like is if one list has a different number than the other the one with extra shows differences for each property not that the collection has a different size and I tried using comparer.CalculateDifferences(null, formulaItem2) but that is not working either.
ValeraT1982 commented 4 years ago

@kenperregaux can you share your code?

  1. It looks like you don't pass factory to your comparer and default one is used.
  2. I think something like comparer.CalculateDifferences(new FormulaItem(), formulaItem2) should work
kenperregaux commented 4 years ago

` internal class TestFactory : ComparersFactory { public override ObjectsComparer.IComparer GetObjectsComparer(ComparisonSettings settings = null, BaseComparer parentComparer = null) { if (typeof(T) != typeof(IList)) { return base.GetObjectsComparer(settings, parentComparer); }

        var comparer = new CustomFormulaItemsComparer(settings, parentComparer, this);

        return (ObjectsComparer.IComparer<T>)comparer;

    }
}
internal class CustomFormulaItemsComparer : AbstractComparer<IList<FormulaItem>>
{
    public CustomFormulaItemsComparer(ComparisonSettings settings, BaseComparer parentComparer, IComparersFactory factory) : base(settings, parentComparer, factory)
    {
    }

    public override IEnumerable<Difference> CalculateDifferences(IList<FormulaItem> obj1, IList<FormulaItem> obj2)
    {
        if (obj1 == null && obj2 == null)
        {
            yield break;
        }

        if (obj1 == null || obj2 == null)
        {
            yield return new Difference("", DefaultValueComparer.ToString(obj1), DefaultValueComparer.ToString(obj2));
            yield break;
        }

        if (obj1.Count != obj2.Count)
        {
            yield return new Difference("Count", obj1.Count.ToString(), obj2.Count.ToString(),
                    DifferenceTypes.NumberOfElementsMismatch);
        }

        var list2 = obj2.ToList();
        var ids = new List<long>();
        foreach (var formulaItem in obj1)
        {
            var formulaItem2 = list2.FirstOrDefault(fi => fi.Id == formulaItem.Id);

            if (formulaItem2 == null)
            {
                formulaItem2 = new FormulaItem();
            }
            else
            {
                ids.Add(formulaItem.Id);
            }

            var comparer = Factory.GetObjectsComparer<FormulaItem>();
            foreach (var difference in comparer.CalculateDifferences(formulaItem, formulaItem2))
            {
                yield return difference.InsertPath($"[Id={formulaItem.Id}]");
            }
        }

        var formulatItem1 = new FormulaItem();
        foreach (var formulaItem2 in obj2)
        {
            if (!ids.Contains(formulaItem2.Id))
            {
                var comparer = Factory.GetObjectsComparer<FormulaItem>();

                foreach (var difference in comparer.CalculateDifferences(formulatItem1, formulaItem2))
                {
                    yield return difference.InsertPath($"[Id={formulaItem2.Id}]");
                }
            }
        }
    }
}

internal class Formula
{
    public long Id { get; set; }
    public string Name { get; set; }
    public IList<FormulaItem> Items { get; set; }
}

internal class FormulaItem
{
    public long Id { get; set; }
    public int Delay { get; set; }
    public string Name { get; set; }
    public string Instruction { get; set; }
}

// Then to test I used... var tf = new TestFactory(); var tc = tf.GetObjectsComparer(); 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 2", Items = new List { new FormulaItem { Id = 1, Delay = 80, Name = "Item One", Instruction = "Instruction One" }, new FormulaItem { Id = 2, Delay = 100, Name = "Item Two", Instruction = "Instruction Two" } } }; var isEqual = tc.Compare(formula1, formula2, out var tds);

The code works because in tds I see the differences. But I was confused because I couldn't set a break point.

Part of my understanding problem is the GetObjectsComparer of the factory only gets called once for the Formula (I would have thought it would be called twice once for Formula and once for IList)

kenperregaux commented 4 years ago

Also, I added a simple dictionary to the Formula class so I assumed I would need another converter...so I added one but doesn't seem to get called...here is my factory code now...is this wrong?

    internal class TestFactory : ComparersFactory
    {
        public override ObjectsComparer.IComparer<T> GetObjectsComparer<T>(ComparisonSettings settings = null,
            BaseComparer parentComparer = null)
        {
            if (typeof(T) == typeof(IList<FormulaItem>))
            {
                return (ObjectsComparer.IComparer<T>)new CustomFormulaItemsComparer(settings, parentComparer, this);
            }

            if (typeof(T) == typeof(Dictionary<string, string>))
            {
                return (ObjectsComparer.IComparer<T>)new CustomElementsComparer(settings, parentComparer, this);
            }

            return base.GetObjectsComparer<T>(settings, parentComparer);
        }
    }
ValeraT1982 commented 4 years ago

1) Probably condition typeof(T) == typeof(IList) isn't correct. Do this. 2) You should have generic parameter or type argument for GetObjectsComparer method. I don't see neither parameter no argument.

P.S. Can you please fix code formatting, it's difficult to read?

ValeraT1982 commented 4 years ago

"Part of my understanding problem is the GetObjectsComparer of the factory only gets called once for the Formula (I would have thought it would be called twice once for Formula and once for IList)"

Yes. It should be called twice.

kenperregaux commented 4 years ago

Hi I seemed to have solve most of my problems, thank so much for the help. I do have one more question. I have tried to use the IgnoreMember api on a comparer but it is not defined and I can't seem to find it anywhere in the definitions of your classes or interfaces. Thanks again!

ValeraT1982 commented 4 years ago

@kenperregaux, what exactly "not defined"? https://github.com/ValeraT1982/ObjectsComparer#ignoring-members describes how to use it.

kenperregaux commented 4 years ago

var comparer = factory.GetObjectsComparer(); comparer.IgnoreMember("Id");

Getting the error: 'IComparer' does not contain a definition for 'IgnoreMember' and no accessible extension method 'IgnoreMember' accepting a first argument of type 'IComparer' could be found (are you missing a using directive or an assembly reference?)

So I was using it (or so I thought) exactly how the example shows.

ValeraT1982 commented 4 years ago

IgnoreMember method are part of BaseComparer. I need to add them to IBaseComparer also, I'll fix it in the next version. The simplest workaround is to use AddComparerOverride methods and use DoNotCompareValueComparer as comparer.

Thank you for finding this issue!!!

kenperregaux commented 4 years ago

Ah OK thanks!

Sorry but I still have one more question, I want to show my users the full object when a new item is added to a collection. Giving the comparer a new of the object doesn't work because some properties are the defaults. Is there a way to create a custom comparer which loops through all the properties in the object and returns a defference with MissedElementInFirstObject difference type?

ValeraT1982 commented 4 years ago

@kenperregaux. no worries 😉 . The only way to do it is to implement it in your custom comparer. I don't see any existing comparer pieces that can help you with this.