views:

310

answers:

2

I have the following code:


var tagToPosts = (from t2p in dataContext.TagToPosts
                                    join t in dataContext.Tags on t2p.TagId equals t.Id
                                    select new { t2p.Id, t.Name });
//IQueryable tag2postsToDelete;
foreach (Tag tag in tags)
{
    Debug.WriteLine(tag);
    tagToPosts = tagToPosts.Where(t => t.Name != tag.Name);
}
IQueryable tagToPostsToDelete = (from t2p in dataContext.TagToPosts
                                            join del in tagToPosts on t2p.Id equals del.Id
                                            select t2p);
dataContext.TagToPosts.DeleteAllOnSubmit(tagToPostsToDelete);

where tags is a List<Tag> - list of tags created by constructor, so they have no id. I run the the code putting a brakepoint on line with Debug and wait for a few cycles to go. Then I put tagToPosts.ToList() in the Watch window for query to execute. In SQL profiler I can see the following query:

exec sp_executesql N'SELECT [t0].[Id], [t1].[Name]
FROM [dbo].[tblTagToPost] AS [t0]
INNER JOIN [dbo].[tblTags] AS [t1] ON [t0].[TagId] = [t1].[Id]
WHERE ([t1].[Name]  @p0) AND ([t1].[Name]  @p1) AND ([t1].[Name]  @p2)',N'@p0 nvarchar(4),@p1 nvarchar(4),@p2 nvarchar(4)',@p0=N'tag3',@p1=N'tag3',@p2=N'tag3'

We can see every parameter parameter have the value of last tag.Name in a cycle. Have you any idea on how to get arout this and get cycle to add Where with new condition every tyme? I can see IQueryable stores only pointer to variable before execution.

+5  A: 

Change your foreach to:

foreach (Tag tag in tags){
    var x = tag.Name;
    tagToPosts = tagToPosts.Where(t => t.Name != x);
}

The reason behind this is lazy evaluation and variable capturing. Basically, what you are doing in the foreach statement is not filtering out results as it might look like. You are building an expression tree that depends on some variables to execute. It's important to note that the actual variables are captured in that expression trees, not their values at the time of capture. Since the variable tag is used every time (and it's scope is the whole foreach, so it won't go out of scope at the end of each iteration), it's captured for every part of the expression and the last value will be used for all of its occurrences. The solution is to use a temporary variable which is scoped in the foreach so it'll go out of scope at each iteration and it the next iteration, it'll be considered a new variable.

Mehrdad Afshari
+1  A: 

Yes, Mehrdad's answer is correct. When a closure captures a variable in C# (which is what is happening when your "Where" lambda refers to the variable "tag"), the compiler applies a pretty precise rule about how to do the capture. If the captured variable is within the same scope, as determined by the surrounding { and } brackets, then the value of that variable will be captured as it is. If it is outside the scope, then only a reference to that variable will be captured. In your original post, the "tag" variable had already cycled through the entire loop to its last value. But if you make the modification Mehrdad suggested, then you are capturing a variable in your same scope, so the individual value of the variable will be embedded into your closure, giving you the results you want.

By the way, you may say, "Yes, but the "tag" variable is in the same scope." But it's not really, because under the hood, the compiler turns a for-each into something like this (this is VERY rough, just to show what happens with the brackets):

{ 
   var iterator = GetTheIterator();
   { 
      while(iterator.MoveNext())
      {
          // Your loop code here
      }
   }
}

Point is, your iterator for a for-each will always be in a scope outside of the one where your loop code is.

Charlie Flowers