Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Building a LINQ query programmatically without local variables tricking me

Tags:

c#

linq

Assume my objects are in perfect working order (i.e. TDD makes me think they work).

I have a list that I create like this (except indented properly):

var result = from v in vendors
             from p in v.Products
             orderby p.Name
             select p;

This works - I get all products from all vendors.

Now I have a list of conditions, built up at runtime by the user. Let's apply them:

foreach (Attribute a in requiredAttributes)
{
    result = result.Where(p => p.Attributes.Contains(a));
}

This may be primitive, but I thought it'd work. However, after this foreach loop is finished, when you enumerate "result" it will contain all products that has the LAST Attribute of the requiredAttributes collection in it's Attributes property (also a collection).

To me, this smells like "a" is overwritten somewhere with each trip trough the loop, and only the last one applies.

Short of somehow writing an extension method to IEnumerable called ContainsAll(IEnumerable) or something to that effect, how can I achieve what I want, which is basically a logical AND, giving me only those products that has ALL the required attributes?

like image 532
Rune Jacobsen Avatar asked Oct 10 '08 05:10

Rune Jacobsen


3 Answers

(Edited for clarity.)

The problem is the foreach loop, and the fact that the "a" variable is being captured and then changed each time. Here's a modification which will work, by effectively introducing a "new" variable for each iteration of the loop, and capturing that new variable.

foreach (Attribute a in requiredAttributes)
{
    Attribute copy = a;
    result = result.Where(p => p.Attributes.Contains(copy));
}

Omer's solution is a cleaner one if you can use it, but this may help if your real code is actually more complicated :)

EDIT: There's more about the issue in this closures article - scroll down to "Comparing capture strategies: complexity vs power".

like image 69
Jon Skeet Avatar answered Nov 15 '22 15:11

Jon Skeet


var result = from v in vendors
             from p in v.Products
             where requiredAttributes.All(a => p.Attributes.Contains(a))
             orderby p.Name
             select p;

HTH.

like image 45
Omer van Kloeten Avatar answered Nov 15 '22 16:11

Omer van Kloeten


I haven't coded it up, but change

foreach (Attribute a in requiredAttributes){    
    result = result.Where(p => p.Attributes.Contains(a));
}

to

foreach (Attribute a in requiredAttributes){    
    Attribute b = a;
    result = result.Where(p => p.Attributes.Contains(b));
}

should fix it too, I think.

like image 40
Brian Avatar answered Nov 15 '22 15:11

Brian