2

I have an expression returning method such as:

    public Expression<Func<User, bool>> GetUserPredicate(int userID)
    {
        return u => u.ID == userID;
    }

(In the original code, this is a longer and more complex piece of logic that I want to reuse so that I extrcted it out into such a method.)

I can use it in queries for Users without a problem:

    var user = dbContext.Users
        .Where(GetUserPredicate(1))
        .Single();

But I also want to use it when querying other entities like Posts:

    var post = dbContext.Posts
        .Where(p => p.ID == 1)
        .Where(p.User => GetUserPredicate(1))
        .Single();

But this does not work. How can it be accomplished?

John L.
  • 1,825
  • 5
  • 18
  • 45
  • What the compiler says? – CodeNotFound Jun 01 '19 at 15:21
  • @CodeNotFound p does not exist in the context – John L. Jun 01 '19 at 15:23
  • You should use `.Where(p => GetUserPredicate(1)` instead of `.Where(p.User => GetUserPredicate(1))` – CodeNotFound Jun 01 '19 at 15:33
  • Then it says cannot implicitly convert Expression> to bool. – John L. Jun 01 '19 at 15:50
  • Problem with more complicated logic is that there is a good chance it won't translate into sql and will end up pulling everything and doing stuff in memory. – Filip Cordas Jun 01 '19 at 16:00
  • @FilipCordas Actually what I meant by complicated is that it includes some switch cases. Otherwise there does not seem to be anything that will cause a problem in terms of translation. – John L. Jun 01 '19 at 16:09
  • Honestly I think it would be good to give a concrete example because EF is difficult to tell what and how it will be supported. You can always build the expression tree manually but that might be more complicated and not worth implementing. – Filip Cordas Jun 01 '19 at 16:16
  • 2
    The answer from the question marked as "duplicate" allows you to compose `Expression>` from `Expression>` and `Expression>`. See also [LINQKit](https://github.com/scottksmith95/LINQKit#plugging-expressions-into-entitysets--entitycollections-the-problem), [NeinLinq](https://github.com/axelheer/nein-linq) and similar which explain and try to address the issue with expression composition more generally. – Ivan Stoev Jun 01 '19 at 16:32
  • @IvanStoev Thanks for pointing out these resources. Before trying the solution in the duplicate post you mentioned, I tried using LinqKit's AsExpandable() with Joelius's answer below but it threw this exception "Unable to cast object of type 'System.Linq.Expressions.FieldExpression' to type 'System.Linq.Expressions.LambdaExpression'." Do you have any idea why? – John L. Jun 01 '19 at 17:17
  • @IvanStoev Here is my final take: I tried the solution in the referred post and it worked. But I could not find a solution to this exact problem in neither LinqKit nor NeinLinq. I suppose you referenced them as a general resource regarding the topic of Expression manipulations, right? Is there a common library that provides this functionality out-of-the-box? – John L. Jun 01 '19 at 18:18
  • 1
    LinqKit provides custom extension method `Invoke` which works in conjunction with `AsExpandable()`. So the usage is `var userPredicate = GetUserPredicate(1); var post = dbContext.Posts.AsExpandable.Where(p => userPredicate.Invoke(p.User)`. Nether C# nor BCL provides such functionality out-of-the-box. – Ivan Stoev Jun 01 '19 at 20:17

2 Answers2

0

You could create a generic method. The problem is that you need to access properties of unrelated classes. But this is not possible with generics, unless a generic type constraint makes these properties available. Idea: define the required properties in interfaces:

public interface IUserProvider
{
    int UserID { get; set; }
}

public class User : IUserProvider ...
public class Post : IUserProvider ...

Then define the method as

public Expression<Func<TEntity, bool>> GetUserPredicate<TEntity>(int userID)
    where TEntity : IUserProvider
{
    return e => e.UserID == userID;
}
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • But Post does not have a UserID property, it has a User navigation property which has an ID property. And it is the same with other entities. – John L. Jun 01 '19 at 15:46
  • Either `GetUserPredicate` always returns the same predicate and must operate on comparable properties or then use 2 different methods `GetUserPredicateFromUser` and `GetUserPredicateFromPost`. – Olivier Jacot-Descombes Jun 01 '19 at 16:00
  • And why not add a `UserID`? You can have a navigation property + foreign key with EF. – Olivier Jacot-Descombes Jun 02 '19 at 12:26
0

Edit:
My solution uses the compiled function of the expression. Since EF only looks at the expression tree and translates it to Sql, it cannot process compiled functions because these are not expression trees (see comment by Filip Corades below).
If you are using my code for other things that can work with compiled functions, you shouldn't have any issues. For this question however it isn't the right answer.

Original answer:

You can add another method which uses the first one.

public Expression<Func<Post, bool>> GetPostUserPredicate(int userID)
{
    Expression<Func<User, bool>> expr = GetUserPredicate(userID);
    Func<User, bool> func = expr.Compile();
    return post => func(post.User);
}

What this does is it compiles the function of the user-expression and uses it to make the expression fit for the post. You can call it like this (adjusted to your code):

var post = dbContext.Posts
        .Where(p => p.ID == 1)
        .Where(GetPostUserPredicate(1))
        .Single();

You could also inline this instead of using an extra function but for me this is a bit too messy. I'll add it anyway:

var post = dbContext.Posts
    .Where(p => p.ID == 1)
    .Where(p => GetUserPredicate(1).Compile()(p.User))
    .Single();

If there isn't something I'm missing about compiling expressions, this should work absolutely fine.
Hope this helps and let me know if it worked.

Joelius
  • 3,839
  • 1
  • 16
  • 36
  • 1
    A compiled function won't translate into sql. – Filip Cordas Jun 01 '19 at 15:58
  • @FilipCordas Really? I was concerned something like that might be the case. I'll not remove it yet, not until I have an answer from OP to see if it works (I obviously don't have their exact setup). – Joelius Jun 01 '19 at 16:02
  • It says "The LINQ expression node type 'Invoke' is not supported in LINQ to Entities." :( Isn't there another way to make it work? – John L. Jun 01 '19 at 16:06
  • No what Ef does it takes the c# expression tree and translates it to Sql since compiled functions are not expression trees there is no way to get the original function. – Filip Cordas Jun 01 '19 at 16:11
  • @FilipCordas I guess somehow the expression should be converted (transformed) without a call to Compile then? – John L. Jun 01 '19 at 16:12
  • I don't think that's possible. That's the reason you got stuck on it.. because maybe it just doesn't work. I can't guarantee that though. – Joelius Jun 01 '19 at 16:14
  • Nvm, I just saw the duplicate - there might just be a way :) But it seems like a really crazy solution. – Joelius Jun 01 '19 at 16:28
  • There is a way to do it by translating the injecting the accessor expression into the expression you want but to me it's much more complicated and not worth implementing in most situations. – Filip Cordas Jun 01 '19 at 16:32