Creating a IEqualityComparer<IEnumerable<T>>

12,557

Solution 1

I just verified that this works fine with xUnit.net 1.9.2:

public class MyClass
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class MyClassComparer : IEqualityComparer<MyClass>
{
    public bool Equals(MyClass x, MyClass y)
    {
        return x.ID == y.ID;
    }

    public int GetHashCode(MyClass obj)
    {
        return obj.ID.GetHashCode();
    }
}

public class ExampleTest
{
    [Fact]
    public void TestForEquality()
    {
        var obj1 = new MyClass { ID = 42, Name = "Brad" };
        var obj2 = new MyClass { ID = 42, Name = "Joe" };

        Assert.Equal(new[] { obj1 }, new[] { obj2 }, new MyClassComparer());
    }
}

So I'm not 100% clear why you need the extra comparer. Just the single comparer should be sufficient.

Solution 2

Well, your implementation is pending. You implemented custom comparer for IEnumerable<KeywordSchedule> but forgot to implement the same for KeywordSchedule.

x.SequenceEqual Still uses Comparer<T>.Default so it goes for reference comaprison and hence result is false.

public class KScheduleComparer : IEqualityComparer<KeywordSchedule>
{
    public bool Equals(KeywordSchedule x, KeywordSchedule y)
    {
        return x.Id == y.Id;                
    }

    public int GetHashCode(KeywordSchedule obj)
    {
        return obj.GetHashCode();
    }
}

Then modify your Equals method in KeywordScheduleComparer class as below

public class KeywordScheduleComparer : IEqualityComparer<IEnumerable<KeywordSchedule>>
{
    public bool Equals(IEnumerable<KeywordSchedule> x, IEnumerable<KeywordSchedule> y)
    {
        return Object.ReferenceEquals(x, y) || (x != null && y != null && x.SequenceEqual(y, new KScheduleComparer()));
    }

    public int GetHashCode(IEnumerable<KeywordSchedule> obj)
    {
        if (obj == null)
            return 0;

        return unchecked(obj.Select(e => e.GetHashCode()).Aggregate(0, (a, b) => a + b));  // BAD
    }
}
Share:
12,557
Jon
Author by

Jon

Updated on June 28, 2022

Comments

  • Jon
    Jon almost 2 years

    I'm using xUnit and it doesn't have a way to determine if 2 IEnumerable<T> are equal if T is custom type.

    I've tried using LINQ SequenceEqual but again as the instances of T are different this returns false;

    Here is a basic test with a non-working IEqualityComparer

        [Fact]
        public void FactMethodName()
        {
            var one = new[] { new KeywordSchedule() { Id = 1 } };
            var two = new[] { new KeywordSchedule() { Id = 1 } };
    
            Assert.Equal(one, two, new KeywordScheduleComparer());
        }
    
    public class KeywordScheduleComparer : IEqualityComparer<IEnumerable<KeywordSchedule>>
    {
        public bool Equals(IEnumerable<KeywordSchedule> x, IEnumerable<KeywordSchedule> y)
        {
            return Object.ReferenceEquals(x, y) || (x != null && y != null && x.SequenceEqual(y));
        }
    
        public int GetHashCode(IEnumerable<KeywordSchedule> obj)
        {
            if (obj == null)
                return 0;
    
            return unchecked(obj.Select(e => e.GetHashCode()).Aggregate(0, (a, b) => a + b));  // BAD
        }
    }
    

    I'm using this in an integration test, so I insert data from a IEnumerable into a DB at the start, then call my SUT to retrieve data from DB and compare.

    If you can help me get a collection comparison working I'd appreciate it!

  • Jon
    Jon over 10 years
    So put public bool Equals(IEnumerable<KeywordSchedule> x, IEnumerable<KeywordSchedule> y) on the KeywordSchedule class?
  • Sriram Sakthivel
    Sriram Sakthivel over 10 years
    No It should be in KeywordScheduleComparer class. If not clear I'll update full code
  • Jon
    Jon over 10 years
    Thanks. Why are there 2 comparers?
  • Servy
    Servy over 10 years
    @Jon Because you need to be able to compare two different types of things, the underlying items, and sequences of items. If you only needed to compare one type you'd only need one comparer.
  • Servy
    Servy over 10 years
    Your first comparer should return obj.Id.GetHashCode() instead. That Id is very important.
  • Sriram Sakthivel
    Sriram Sakthivel over 10 years
    @Jon I think servy answered your question. Or you could simply do bool res = one.SequenceEqual(two, new KScheduleComparer()); and you can get rid of second comparer.
  • Jon
    Jon over 10 years
    @BradWilson Thats what I was doing I think in question I think so not sure the difference
  • Jon
    Jon over 10 years
    Ah thanks, I wasnt too far off. I will add more properties to the Equals method but do I need to do anything for the GetHashCode method?
  • Brad Wilson
    Brad Wilson over 10 years
    Technically you're supposed to implement GetHashCode, but if the comparer is for xUnit.net only, you can skip it, as we don't use GetHashCode. Just leave yourself a note to implement it if you ever decide to use the thing for real. :)
  • Hilarion
    Hilarion over 2 years
    Although I agree, that FluentAssertions are a good solution to many such issues, your example does not directly address the question. I.e. you are not asserting if two lists are equal (you only have one) and, if I remember correctly, default use of BeEquivalentTo disregards the order of items in compared collections, which is probably not the desired result (as the example in the question used SequenceEqual). Also, when using FluentAssetions, you actually don't need to have Equals overriden in the MyClass - you can provide comparison rules to FluentAssertions to compare by properties.