Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass a lambda parameter to an include statement

I am using the System.Data.Entity namespace, so I can pass lambda expressions to the Linq Include method.

public ICollection<MyEntity> FindAll()
    {
        using (var ctx = new MyEntityContext())
        {
            return ctx.MyEntity.Include(x => x.SomeLazyLoadedValue).ToList();
        }
    }

When I'm using a Where statement in a different method, I can pass a parameter to it like so:

public ICollection<MyEntity> FindAllBy(Func<MyEntity, bool> criteria)
    {
        using (var ctx = new MyEntityContext())
        {
            return ctx.MyEntity.Where(criteria).ToList();
        }
    }

However, trying the same thing in an Include does not work:

public ICollection<MyEntity> FindAll(Func<MyEntity, bool> criteria)
    {
        using (var ctx = new MyEntityContext())
        {
            return ctx.MyEntity.Include(criteria).ToList();
        }
    }

If you try this, Visual Studio will complain that it

Cannot convert from 'System.Func<MyEntity, bool>' to 'string'

How do I pass a lambda to the Include method?

like image 997
yesman Avatar asked Mar 09 '16 10:03

yesman


1 Answers

There are a few problems with your code. For instance, your FindAllBy does not do a sql WHERE query, instead it loads all the entries in your database, and then filter in-memory based on your criteria. To understand why this is like so take a look at the following:

int a = 5;
long b = 5;

Now, it's quite obvious what's happening here, but it's still quite important. The compiler reads the following code and produces two variables. One integer and one long integer, both with values set to the number 5. However, the values of these two numbers are different, even though they are set (in the source code) to the same thing. One is 32-bit, and the other is 64-bit.

Now, let's take a look at the following code:

Func<int, string> a = num => num.ToString();
Expr<Func<int, string>> b = num => num.ToString();

Here the same thing (more or less) is happening. In the first case, the C# compiler sees you want a predicate (a Func<int, string> predicate), whereas the second value is a Expr<Func<int, string>> even though the values are written the same. However, as opposed to the first example, the end result here is vastly different.

A predicate is compiled as a method on a compiler-generated class. It's compiled just as any other code, and simply allows you to remove a bunch of boilerplate. A expression on the other hand is a in-memory representation of the actual code written. In this case, for instance, the expression might look something akin to Call(int.ToString, $1). This can be read by other code and translated to for instance SQL which is then used to query your database.

Now, back to your problem. EntityFramework hands you IQueryable<T> instances, which in turn inherit IEnumerable<T>. Whenever you enumerate over the enumerable, it queries the database.

All the extension-methods that accept delegates are defined on IEnumerable and thus query your database before running the predicate. This is why you need to make sure to select the right method-overloads.

Edit (to answer comment)]
To clarify a bit more I'm going to make a few examples. Say for instance that we have a User class that cointains FirstName, LastName and Age, and the db collection is simply called db.

Expr<Func<User, bool>> olderThan10 = u => u.Age > 10;
Func<User, bool> youngerThan90 = u => u.Age < 90;
var users = db.Where(olderThan10).Where(youngerThan90);

This would result in SQL that finds all users that are older than 10, after which it would in-memory filter away everyone that was older than or equal to 90.

So passing a Func doesn't necessarily mean it queries the whole database. It just means it stops building on the query at that point, and executes it.

As for the next question, Expression<Func<T,bool>> is not a universal answer. It means "a expression that takes a T and returns a bool". In some cases, like .Include which started this whole question, you don't want to return a bool. You want to return whatever you want to include. So for instance, if we go back to our example of users, and amend a Father property on the user class which references another user, and we want to include it, in regular code we'd do

db.Include(u => u.Father);

Now. Here, u is a User, and the return value u.Father is also a user, so in this case u => u.Father is Expr<Func<User, User>> or Expr<Func<User, object>> (I don't know if entity-framework .Include accepts generic values or simply objects).

So your FindAll function should probably look like this:

public ICollection<TData> FindAll<TInclude>(Expr<Func<TData, TInclude>> include) {
    using (var ctx = new TContext()) {
        return ctx.T.Include(include).ToList();
    }
}

Though, to be honest, this is pretty weird looking code, and it's likely that you're doing something else weird with your models given that you've (for instance) named them T and TContext. My guess is that you need to read up a bit on how generics works in C#.

like image 144
Alxandr Avatar answered Sep 26 '22 13:09

Alxandr