Darren Darren - 8 months ago 44
C# Question

Linq query to find a match where nested list contains sequence

Given these two objects:

public class Foo{
public string Result {get;set;}
public List<Bar> Bars {get;set;}
}

public class Bar{
public string Left {get;set;}
public string Right {get;set;}
}


And instances of these looking like this:

List<Foo> myFoos = new List<Foo>()
{
new Foo { Bars = new List<Bar>
{
new Bar { Left = "myLeft1", Right = "myValue1"},
new Bar { Left = "myLeft2", Right = "myValue2"}
},
Result = "TheWinningResult"},
new Foo { Bars = new List<Bar>
{
new Bar { Left = "myLeft2", Right = "myValue2"},
new Bar { Left = "myLeft3", Right = "myValue3"}
},
Result = "TheLosingResult"},
new Foo{ Bars = new List<Bar>
{
new Bar { Left = "myLeft1", Right = "myValue1"},
new Bar { Left = "myLeft2", Right = "myValue2"},
new Bar { Left = "myLeft3", Right = "myValue3"}
},
Result = "TheOtherLosingResult"},
};

List<Bar> bars = new List<Bar>()
{
new Bar{ Left = "myLeft1", Right = "myValue1" },
new Bar{ Left = "myLeft2", Right = "myValue2" }
};


I am trying to find the
FirstOrDefault()
Foo
where
Foo.Bars
has an exact matching
bars


In this case, I am trying to return the
Foo
whos
Result
is "TheWinningResult"

I have tried the following:

Foo foo = myFoos.Where(t => t.Bars.All(t2 => bars.Contains(t2))).FirstOrDefault();

Foo foo = myFoos.Where(t => bars.All(r => t.Bars.Contains(r))).FirstOrDefault();

Foo foo = myFoos.FirstOrDefault(t => t.Bars.Any(r => bars.All(ru => ru.Left == r.Left && ru.Right == r.Right)));


Any idea where I am going wrong?

Update

I forgot to mention, Order of the
Bars
within
Foo
should not matter.

Answer Source

The problem is in how contains compares the objects. By default comparison is done by reference equality. Here you want a different behavior - compare equality by content. To do so override Equals and GetHashCode of Bar:

public override bool Equals(object obj)
{
    var other = obj as Bar;
    if (obj == null)
        return false;
    return other.Left == Left && other.Right == Right;
}

public override int GetHashCode()
{
    unchecked
    {
        int hash = 17;
        hash = hash * 23 + Left.GetHashCode();
        hash = hash * 23 + Right.GetHashCode();
        return hash;
    }
}

And then retrieving the correct objects can be done by:

var result = myFoos.Where(item => bars.Count == item.Bars.Count && 
                                  !bars.Except(item.Bars).Any());

Or if using SequenceEqual:

var result = myFoos.Where(item => bars.SequenceEqual(item.Bars));

Another way to do it is by implementing the IEqualityComparer<Bar> and calling the matching Contains overload:

var result = myFoos.Where(t => t.Bars.All(t2 => bars.Contains(t2, new BarEqualityComparer())));

As for your third attempt the problem is that the All and Any is the other way round: you want for all bars in the foo to match to any of the items in the bars list:

Foo foo = myFoos.FirstOrDefault(t => t.Bars.All(r => bars.Any(ru => ru.Left == r.Left && ru.Right == r.Right)));

For what the override of the GetHashCode is for you can read these two:

  1. Object.GetHashCode
  2. Why is it important to override GetHashCode when Equals method is overridden?
Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download