Stefan Rusek | LINQ – Beware of First() and Count()

LINQ – Beware of First() and Count()

Saturday, August 22, 2009

LINQ is one of my favorite things about C# 3. In fact, I rewrote almost all of the LINQ extension methods a few months ago, so I could use LINQ in a .NET v2 application. However, LINQ can also be a huge source of programmer error. There are a few extension methods that are particularly dangerous, but by far I’ve seen the most problems with the First() and the Count() methods especially when used with database based queries. The problem occurs because LINQ is IEnumerator based. LINQ doesn’t do anything magical for these two methods, so we can assume the default implementations look something like the following:

public T First<T>(this IEnumerable<T> src)
{
    foreach (T item in src)
        return item;
    throw new ItemNotFoundException();
}

public int Count<T>(this IEnumerable<T> src)
{
    int c = 0;
    foreach (T item in src)
        c++;
    return c;
}

The problem here is that both of these methods start the iteration over again. This means that the following code inadvertently results in 2 database hits when it finds a user:

public User GetUser(int userid)
{
    var q = GetUserQuery(userid);
    if (q.Count() > 0)
        return q.First();
    else
        return User.DefaultUser;
}

Most of the time we can fix this pretty easily by either using ToList() or FirstOrDefault(), which are implemented something like this:

public T FirstOrDefault<T>(this IEnumerable<T> src)
{
    foreach (T item in src)
        return item;
    return default(T);
}

public List<T> ToList<T>(this IEnumerable<T> src)
{
    return new List<T>(src);
}

And so GetUser() becomes one of the following:

public User GetUser1(int userid)
{
    var u = GetUserQuery(userid).ToList();
    if (u.Count > 0)
        return u[0];
    else
        return User.DefaultUser;
}

public User GetUser2(int userid)
{
    return GetUserQuery(userid).FirstOrDefault() ?? User.DefaultUser;
}

Jacob Is Not Valid said...

One more way to write GetUser is:

public User GetUser3(int userid)

{

GetUserQuery(userid).DefaultIfEmpty(User.DefaultUser).First();

}

And, of course, if you have some reason to check if an enumeration is empty, it is fastest to use rg.Any()

on Thursday, July 15, 2010
Stefan Rusek said...

I don't think DefaultIfEmpty() is all that useful when combined with First().

It does makes more sense when you want to do something similar to python's else clause on a for loop.

on Thursday, July 15, 2010
Jacob Is Not Valid said...

Why isn't it useful? DefaultIfEmpty guarantees at least one element will be present, and First grabs the first one :)

on Saturday, July 17, 2010
Please log in or register to comment.