EF 4.0 / Weird behaviour of FirstOrDefault method

2020-06-25 04:33发布

问题:

I am developing a small application using EF 4.0 and POCO.

While testing my application, I grew concerned about the performance of the Data Access Layer. So I fired SQL Profiler to see that when trying to retrieve a record:

ctx.Orders.Include("OrderItems").FirstOrDefault<Order>(c => c.OrderID == id);

the EF issues a SQL statement that would retrieve all records from the Orders table on the Server and as such return to DAL at which time L2E would pick one thay meet the criteria and return it.

Can this behaviour be changed.

Thanks!

Zen

回答1:

Try this one please:

ctx.Orders.Include("OrderItems").Where(c => c.OrderID == id).FirstOrDefault();


By the way you don't need to look into SQL Profiler to see the generated SQL, you can do it right inside your code by writing:

IQueryable<Order> query = ctx.Orders.Include("OrderItems")
                                    .Where(c => c.OrderID == id);
string sql = ((ObjectQuery<Order>)query).ToTraceString();


EDIT:
Question: What if we have a function like FindOrders and we need to pass the predicate to this function? Answer: The code should looks like:

public List<Order> FindOrders(Expression<Func<Order, bool>> predicate) { 
    using (DBContext ctx = new DBContext()) { 
        return ctx.Orders.Include("OrderItems").Where(predicate).ToList<Order>(); 
    } 
} 

//Calling the function:
var order = FindOrders(c => c.OrderID == id)[0];


This time, if you check your SQL Profiler you'll see there is a where clause in the SQL that's been submitted to SQL Server.

Explanation:
The reason for this "Weird behavior" is that basically when you write Where(c => c.OrderID == id), C# compiler cast your lambda expression into an Expression<Func<TSource, int, bool>> and NOT to a Func<TSource, int, bool>.

MSDN Documentation for Queryable.Where also confirms this:

public static IQueryable<TSource> Where<TSource>(
    this IQueryable<TSource> source,
    Expression<Func<TSource, int, bool>> predicate
)

However if you explicitly pass a Func<TSource, int, bool>> to the Where method then you are basically calling Enumerable.Where:

public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, int, bool> predicate
)

And as we know IEnumerable.Where is "LINQ to Objects" implementation and NOT "LINQ to Entities" which means onces you reach to your IEnumerable.Where call, the ObjectQuery run the initial query (ctx.Orders.Include("OrderItems")) and gives the results to the IEnumerable.Where so that it will filter it out for you on the Client Side.

On the other hand the call with Queryable.Where (ctx.Orders.Include("OrderItems").Where(c => c.OrderID == id).FirstOrDefault()) will not be executed until it reach to the point that we call FirstOrDefault() function which means the Queryable.Where is then translated into native SQL along with the rest of the query and will be passed to the SQL Server, hence you see the Where clause on the SQL statement which definitely is the desired runtime behavior.

By the way, don't forget to import this namespace to your class file:
using System.Linq.Expressions;