简体   繁体   中英

Strange behavior with IEnumerable/IQueryable extension (no lazy loading?)

I have some weird behavior I can't figure out.

The two methods below should from what I understand (which is apparently wrong) behave the same way given an IQueryable, but they don't.

If I call the first with an IQueryable (the object is a DbSet from entity framework used explicitly as an IQueryable) it looks like it doesn't use lazy loading (it performs a scan on the database). When I call the second method with the same object it appears to be working the way I want it to (it performs a seek on the database).

So, two questions:

  • Why is this happening?

  • Can I (and how) make the most general method (with IEnumerable) work "properly"? (Since I have more extensions and don't want to duplicate code I would like to avoid overloading and just copy-pasting the method body like below)

I'm using EF 4.1 working against a SQL Server Express 2008 database

public static TEntity GetByID<TEntity>(this IEnumerable<TEntity> list, long id) where TEntity : Identifiable
{
   return list.SingleOrDefault(e => e.ID == id);
}

public static TEntity GetByID<TEntity>(this IQueryable<TEntity> list, long id) where TEntity : Identifiable
{
    return list.SingleOrDefault(e => e.ID == id);
}

IEnumerable<T> does not pass the query expression to the EF LINQ provider, but instead performs the SingleOrDefault() in memory. This requires a complete load of your table to memory, followed by the SingleOrDefault() . By using the IQueryable<T> version, the provider is given the correct expression tree, which it translates to the SQL you want. That's where the difference comes from.

Since list is typed as IEnumerable in your first case the Entity Framework Query provider takes this as a hint to perform the SingleOrDefault() method on it in memory (it will use the Linq methods on Enumerable instead of Queryable ) - that's why you see the database scan to materialize the complete list.

Also see the AsEnumerable() method and this post by Jon Skeet: "Reimplementing LINQ to Objects: Part 36 - AsEnumerable"

For anyone who comes here, one other thing to note:

You need to be absolutely sure you are providing an expression to the SingleOrDefault() method, otherwise there is potential for the following to occur:

public static TEntity GetByID<TEntity>(this IEnumerable<TEntity> list, Func<TEntity,bool> selector) where TEntity : Identifiable
{
   return list.SingleOrDefault(selector);
}

public static TEntity GetByID<TEntity>(this IQueryable<TEntity> list, Func<TEntity,bool> selector) where TEntity : Identifiable
{
    return list.SingleOrDefault(selector);
}

These methods will run exactly the same. WHY? Because you are not providing an expression to the IQueryable<> list, effectively what happens is the list will be auto-converted to an IEnumerable<> and then the selector will be run on the IEnumerable<> . So, in effect, you are actually pulling the entire table/list into memory from the DB, then running the selector on the list in memory.

I just got done by this, and I though I was going crazy.

If you pass in an Expression<Func<TEntity,bool>> to the IQueryable<> , it will be performed DB side.

FirstOrDefault is defined seperately for IEnumerable and IQueryable System.Linq.Queryable.FirstOrDefault() is being called in the 2nd case.

If you want to combine both into one method, you can test if list is IQueryable and use the Queryable extension method instead.

public static TEntity GetByID<TEntity>(this IEnumerable<TEntity> list, long id) where TEntity : Identifiable
{
    var query = list as IQueryable<TEntity>;
    if (query != null)
        return query.SingleOrDefault(e => e.ID == id);
    return list.SingleOrDefault(e => e.ID == id);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM