views:

332

answers:

3

ok guys, bare with me. I'll summarize first, then go into detail.

I've written a number of methods (.WhereOr, .WhereAnd) which basically allow me to "stack up" a bunch of lambda queries, and then apply them to a collection. For example, the usage with datasets would be a little like this (although it works with any class by using generics):

WITH LINQ TO DATASETS (Using the .NET DataSetExtensions)

DataTable Result;

List<Expression<Func<DataRow, bool>> Queries = new List<Expression<Func<DataRow, bool>>();

Queries.Add(dr=> dr.Field<string>("field1") == "somestring");
Queries.Add(dr=> dr.Field<string>("field2") == "somestring"); 
Queries.Add(dr=> dr.Field<string>("field3") == "somestring"); 

Result = GetSomeTable().AsEnumarable().WhereOr(Queries).CopyToDataTable();

Now say that in the example above only one row in the collection matches "somestring", and it's on field "field2".

That means that the count for Result should be 1.

Now, say I re-write the code above slightly to this:

DataTable Result;

List<Expression<Func<DataRow, bool>> Queries = new List<Expression<Func<DataRow, bool>>();

List<string> columns = new string[]{"field1","field2","field3"}.ToList();

string col;

foreach(string c in columns){
    col = c;
    Queries.Add(dr=> dr.Field<string>(col) == "somestring");
}

Result = GetSomeTable().AsEnumarable().WhereOr(Queries).CopyToDataTable();

Now, I don't really understand expressions, but to me both examples above are doing exactly the same thing.

Except that "Result" in the first example has a count of 1, and "Result" in the second example has a count of 0.

Also, in the List columns in the second example, if you put "field2" last, instead of second, then "Result" does correctly have a count of 1.

So, from all this I've come to a kind of conclusion, but I don't really understand what's happening, nor how to fix it..? Can I "evaluate" those expressions earlier...or part of them?

CONCLUSION:

Basically, it seems like, if I send literal values into there, like "field1", it works. But if I send in variables, like "col", it doesn't work, because those "expressions" are only getting evaluated much later in the code.

that would also explain why it works when I move "field2" to the last position. it works because the variable "col" was assigned to "field2" lastly, thus by the time the expressions evaluate "col" equals "field2".

Ok, so, is there any way around this??

Here's the code for my WhereOr method (it's an extension method to IENumerable):

public static IQueryable<T> WhereOr<T>(this IEnumerable<T> Source, List<Expression<Func<T, bool>>> Predicates) {

        Expression<Func<T, bool>> FinalQuery;

        FinalQuery = e => false;

        foreach (Expression<Func<T, bool>> Predicate in Predicates) {
            FinalQuery = FinalQuery.Or(Predicate);
        }

        return Source.AsQueryable<T>().Where(FinalQuery);
    }

public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> Source, Expression<Func<T, bool>> Predicate) {
        InvocationExpression invokedExpression = Expression.Invoke(Predicate, Source.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, bool>>(Expression.Or(Source.Body, invokedExpression), Source.Parameters);
    }
+3  A: 

I don't even bother reading the question bodies any more, I just read the title and then say

See

http://lorgonblog.spaces.live.com/blog/cns!701679AD17B6D310!689.entry

and

http://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx

Brian
(I'll probably be wrong this time by skipping the question, so downvote me if I am!)
Brian
Nah, it's the usual stuff, albeit with a twist (he's not closing over a loop variable, he's assigning to a variable _explicitly_ declared _outside_ the loop, and then closing over that).
Pavel Minaev
Pavel! I don't understand what you've just said, but you're on the money. dude, what am I doing wrong? educate me. cheers man
andy
brian, thanks for the links dude... I gave it to Pavel for the fix, but i definitely want to understand what's going on, so I'll do some researching. cheers!
andy
+1  A: 

Oh geez, after I commented I saw the issue. You are using the same variable, "col", in every iteration of the loop. When you build the lambda expression, it doesn't bind to the value of the variable, it references the variable itself. By the time you execute the query, "Col" is set to whatever it's last value is. Try creating a temporary string inside the loop, setting its value, and using that.

Chris
exactly chris! that's exactly what's happening. What do you mean by setting a temp var though? And, is there a way to "force" a bind of the values? cheers
andy
No, binding is always over variables. But if you declare a variable inside the loop, then you'll get a new variable for each iteration of the loop, and so each lambda will bind to its own (which will not change afterwards, unless the lambda decides to change it).
Pavel Minaev
+3  A: 

The "how to fix" answer. Change this:

string col;
foreach(string c in columns) {
    col = c;
    Queries.Add(dr=> dr.Field<string>(col) == "somestring");
}

to this:

foreach(string c in columns) {
    string col = c;
    Queries.Add(dr=> dr.Field<string>(col) == "somestring");
}

Enjoy. The "what & why" answer was given by Brian.

Pavel Minaev
dude, you are the man! thank you
andy