4

I'm getting the "Possible Multiple Enumeration of IEnumerable" warning from Reshaper. How to handle it is already asked in another SO question. My question is slightly more specific though, about the various places the warning will pop up.

What I'm wondering is whether or not Resharper is correct in giving me this warning. My main concern is that the warning occurs on all instances of the users variable below, indicated in code by "//Warn".

My code is gathering information to be displayed on a web page in a grid. I'm using server-side paging, since the entire data set can be tens or hundreds of thousands of rows long. I've commented the code as best as possible.

Again, please let me know whether or not this code is susceptible to multiple enumerations. My goal is to perform my filtering and sorting of data before calling ToList(). Is that the correct way to do this?

private List<UserRow> GetUserRows(UserFilter filter, int start, int limit,
                                  string sort, SortDirection dir, out int count)
{
    count = 0;

    // LINQ applies filter to Users object
    var users = (
            from u in _userManager.Users
            where filter.Check(u)
            select new UserRow
                        {
                            UserID = u.UserID,
                            FirstName = u.FirstName,
                            LastName = u.LastName,
                            // etc.
                        }
        );

    // LINQ orders by given sort
    if (!String.IsNullOrEmpty(sort))
    {
        if (sort == "UserID" && dir == SortDirection.ASC)
            users = users.OrderBy(u => u.UserID); //Warn
        else if (sort == "UserID" && dir == SortDirection.DESC)
            users = users.OrderByDescending(u => u.UserID); //Warn
        else if (sort == "FirstName" && dir == SortDirection.ASC)
            users = users.OrderBy(u => u.FirstName); //Warn
        else if (sort == "FirstName" && dir == SortDirection.DESC)
            users = users.OrderByDescending(u => u.FirstName); //Warn
        else if (sort == "LastName" && dir == SortDirection.ASC)
            users = users.OrderBy(u => u.LastName); //Warn
        else if (sort == "LastName" && dir == SortDirection.DESC)
            users = users.OrderByDescending(u => u.LastName); //Warn
        // etc.
    }
    else
    {
        users = users.Reverse(); //Warn
    }

    // Output variable
    count = users.Count(); //Warn

    // Guard case - shouldn't trigger
    if (limit == -1 || start == -1)
        return users.ToList(); //Warn

    // Pagination and ToList()
    return users.Skip((start / limit) * limit).Take(limit).ToList(); //Warn
}
Community
  • 1
  • 1
Nick Vaccaro
  • 5,428
  • 6
  • 38
  • 60

2 Answers2

4

Yes, ReSharper is right: count = users.Count(); enumerates unconditionally, and then if the limit or the start is not negative 1, the ToList would enumerate users again.

It appears that once ReSharper decides that something is at risk of being enumerated multiple times, it flags every single reference to the item in question with the multiple enumeration warning, even though it's not the code that is responsible for multiple enumeration. That's why you see the warning on so many lines.

A better approach would add a separate call to set the count. You can do it upfront in a separate statement, like this:

count = _userManager.Users.Count(u => filter.Check(u));

This way you would be able to leave users in its pre-enumerated state until the final call of ToList.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • Just tested out your solution. Looks like you are correct; it was flagging every instance instead of just the enumerations. I'd really like to _not_ have to get the count like that, but there doesn't seem to be a better way. – Nick Vaccaro Dec 03 '12 at 16:27
  • Come to think of it - I imagine it would be faster if I got the count _outside_ of the method, after I've already got the list created. Does that seem correct? – Nick Vaccaro Dec 03 '12 at 16:29
  • @Norla As far as the SQL interaction is concerned, there is no difference between what you did and what's in the workaround: no matter what SQL the EF/LINQ2SQL generate, the optimizer in the RDBMS would transform it to the same, efficient fetch of the record count from the RDBMS. – Sergey Kalinichenko Dec 03 '12 at 16:30
  • This is all in memory - no SQL. – Nick Vaccaro Dec 03 '12 at 16:30
  • @Norla Well, your solution fetches the *raw* count, before the limits get applied. If you switch to getting the count from the `List`, the answer would be equal to `limit` every time you run a query with a limit; I was under impression that you are trying to avoid this. – Sergey Kalinichenko Dec 03 '12 at 16:31
  • @Norla If it's all in memory, you might as well apply `ToList` at the point of declaration of `users`. This will silence the warning for sure, and it also would let you get the pre-limit count very efficiently. – Sergey Kalinichenko Dec 03 '12 at 16:32
  • Nope, it's post-limit. I think running with your solution will be the correct way to go. – Nick Vaccaro Dec 03 '12 at 16:34
  • For posterity's sake, I have to add this comment. The count should _not_ be post-limit. My brain broke immediately before responding. I have added the set 'count' statement as the first line in the method. Man, that's embarrassing. – Nick Vaccaro Dec 06 '12 at 16:43
  • While it's true that `Enumerable.Count()` will enumerate unconditionally in your case, this is not true in general. If the underlying type has a `Count` property (i.e. a `List`), then `Count()` will defer to this constant-time operation. – Matthew Mar 11 '14 at 23:18
1

Your warning is hopefully generated by the call to Count, which does run an extra query.

In the case where limit == -1 || start == -1 you could make the ToList call and then get the count from that, but in the general case there's nothing you can do - you are making two queries, one for the full count and one for a subset of the items.

I'd be curious to see whether fixing the special case causes the warning to go away.


Edit: As this is LINQ-to-objects, you can replace the last return line with a foreach loop that goes through your whole collection counting them, but also builds up your restricted skip/take sublist dynamically, and thus only iterates once.

You could also benefit from only projecting (select new UserRow) in this foreach loop and just before your special-case ToList, rather than projecting your whole collection and then potentially discarding the majority of your objects.

var users = _userManager.Users.Where(u => filter.Check(u));

// Sort as above

List<UserRow> rtn;
if (limit == -1 || start == -1)
{
    rtn = users.Select(u => new UserRow { UserID = u.UserID, ... }).ToList();
    count = rtn.Length;
}
else
{
    int takeFrom = (start / limit) * limit;
    int forgetFrom = takeFrom + limit;
    count = 0;
    rtn = new List<UserRow>();
    foreach(var u in users)
    {
        if (count >= takeFrom && count < forgetFrom)
            rtn.Add(new UserRow { UserID = u.UserID, ... });
        count++;
    }
}
return rtn;
Rawling
  • 49,248
  • 7
  • 89
  • 127
  • Warning is being generated by Resharper on _all_ lines with "// Warn" comment, not just the few on the end. – Nick Vaccaro Dec 03 '12 at 16:13
  • @Norla I wonder if Resharper is putting warnings on all the lines, but will take all the warnings away if you just fix the one line that's causing the trouble? Otherwise it doesn't sound like Resharper's doing a very good job. – Rawling Dec 03 '12 at 16:35
  • Tested solution, and yes, it does remove the warning after the count is fixed. – Nick Vaccaro Dec 03 '12 at 17:01