9

In my C# code, I have 2 WHERE queries both of which I can call on an IQueryable and have the whole thing compiled down to SQL, and both of which have a great deal of common logic.

I believe this is NOT a duplication of this similar question: Using Function in Select Clause of Entity Framework Query because in my scenario the function in question CAN be converted into SQL - EF just isn't realising that it can do so.

The queries are approximately:

public static IQueryable<Template> WhereIsOwnedByUser(this IQueryable<Template> set, User user)
{
    return set.Where(temp =>
        temp.Requests
            .Where(req => req.WasSent)
            .OrderByDescending(req => req.DueDate)
            .Take(2)
            .SelectMany(req => req.RequestRecipients.Select(reqRecip => reqRecip.Recipient.Id))
            .Contains(user.Id));
}

AND

public static IQueryable<Template> WhereIsOwnedByUser(this IQueryable<DataReturn> set, User user)
{
    return set.Where(ret=>
        ret.Entity.Id == user.Entity.Id
        &&
        ret.Request.Template.Requests
            .Where(req => req.WasSent)
            .OrderByDescending(req => req.DueDate)
            .Take(2)
            .SelectMany(req => req.RequestRecipients.Select(reqRecip => reqRecip.Recipient.Id))
            .Contains(user.Id));
}

So a basic BusinessLogic rule for "owns a template" and then a corollary of that for "owns DataReturn if company matches AND owns template"

As you can see, thinking only about the C#, these could easily be refactored as:

private static bool UserOwnsTemplate(User user, Template temp)
{
    return temp.Requests
               .Where(req => req.WasSent)
               .OrderByDescending(req => req.DueDate)
               .Take(2)
               .SelectMany(req => req.RequestRecipients.Select(reqRecip => reqRecip.Recipient.Id))
               .Contains(user.Id);
}

public static IQueryable<Template> WhereIsOwnedByUser(this IQueryable<Template> set, User user)
{
    return set.Where(temp => UserOwnsTemplate(user, temp));
}

public static IQueryable<DataReturn> WhereIsOwnedByUser(this IQueryable<DataReturn> set, User user)
{
    return set.Where(
        ret =>
            ret.Entity.Id == user.Entity.Id
            &&
            UserOwnsTemplate(user, ret.Request.Template)
    );
}

Thus reducing the duplication (Yay!)

But then EF will complain that it doesn't know what to do with UserOwnsTemplate, despite the fact that it can handle the logic in SQL perfectly well.

AFAICT there is no nice way to solve this. I think my options are:

  • Turn UserOwnsTemplate into a UDF, a SQL function defined in the database.
    • But I can't create a UDF from a C# lamda, I have to define the SQL, which would be more hassle.
  • Assign the Expression<Func<Template,bool>> that UserOwnsTemplate defines as a variable, and then build relevant Expression<Func<DataReturn ,bool>> for the DataReturn version by hand using Expression.AndAlso to glue the two "clauses" together.
    • Meta-programming. Ughhh. I've done this before in another project and it was vile to do, and a nightmare to maintain.
  • Live with the duplication.
    • Likely what will happen unless SO can advise otherwise. ;)

Can anyone see any other options available?

Can I do anything to force EF into parsing the function into SQL? (the phrase "inling" comes to mind but I don't 100% know what I think I mean by that?)

Can anyone see a way to convert ret.Request.Template into an IQueryable so that I can just call the other WhereIsOwnedBy extension method on it?

Any other suggestions AT ALL?

Community
  • 1
  • 1
Brondahl
  • 7,402
  • 5
  • 45
  • 74
  • 1
    Regarding your 2nd bullet point, I think the AndAlso bit doesn't require any expression-tree wrangling. You can always do `set.Where(expression).Where(anotherExpression)`. The difficulty comes from the fact that your expression depends on a User, and that you want to apply it to a different path from the root of the IQueryable (temp vs. ret.Request.Template). I can't see how to address these without constructing your own expression. There may be another way around it, but I would expect it to involve substantially changing the queries that you're making. – hgcummings Oct 14 '15 at 16:31
  • Duplicate of http://stackoverflow.com/questions/7066305/how-to-stay-dry-whilst-using-linq-to-entities-and-helper-methods ? – Dan Oct 16 '15 at 13:15

2 Answers2

1

You can keep your syntax and make it work but you will need to call an additional method on the outer IQueryable<>.

The trick is to manually replace the IQueryable<>.Expression with a copy in which you replace the function call by the corresponding Expression>.

So the idea is to do something like that:

public static class MyLinqExtensions
{
    public static IQueryable<T> InlineFunctions<T>(this IQueryable<T> queryable)
    {
        var expression = TransformExpression(queryable.Expression);
        return (IQueryable<T>)queryable.Provider.CreateQuery(expression);
    }

    private static Expression TransformExpression(System.Linq.Expressions.Expression expression)
    {
        var visitor = new InlineFunctionsExpressionVisitor();
        return visitor.Visit(expression);
    }

    private class InlineFunctionsExpressionVisitor : System.Linq.Expressions.ExpressionVisitor
    {
        protected override System.Linq.Expressions.Expression VisitMethodCall(System.Linq.Expressions.MethodCallExpression methodCallExpression)
        {   
            if (methodCallExpression.Method.IsStatic
                && methodCallExpression.Method.DeclaringType == typeof(MyDeclaringType)
                && methodCallExpression.Method.Name == "WhereIsOwnedByUser")
            {
                var setArgumentExpression = methodCallExpression.Arguments[0];
                var userArgumentExpression = methodCallExpression.Arguments[1];
                var methodInfo = ... // Get typeof(IQueryable<Template>).MethodInfo
                var whereConditionExpression = ...// Build where condition and use userArgumentExpression
                return Expression.MethodCallExpression(methodInfo, setArgumentExpression, whereConditionExpression);
            }
            return base.VisitMethodCall(methodCallExpression);


            // Some ideas to make this more flexible:
            // 1. Use an attribute to mark the functions that can be inlined [InlinableAttribute]
            // 2. Define an Expression<Func<>> first to be able to get the Expression and substritute the function call with it:
            // Expression<Func<IQueryable<Template>, User, IQueryable<Template>>> _whereIsOwnedByUser = (set, user) => 
            // {
            //  return set.Where(temp => UserOwnsTemplate(user, temp));
            // };
            //
            // public static IQueryable<Template> WhereIsOwnedByUser(this IQueryable<Template> set, User user)
            // {
            //  // You should cache the compiled expression
            //  return _whereIsOwnedByUser.Compile().Invoke(set, user); 
            // }
            //
        }
    }
}

And then you can do this:

public static IQueryable<DataReturn> WhereIsOwnedByUser(this IQueryable<DataReturn> set, User user)
{
    return set.Where(
        ret =>
            ret.Entity.Id == user.Entity.Id
            &&
            UserOwnsTemplate(user, ret.Request.Template)
    )
    .InlineFunctions();
}
Guillaume86
  • 14,341
  • 4
  • 53
  • 53
  • Some toughts: with an AOP framework (a pre-build step that rewrite some of the code), the runtime cost would then be zero. You would just mark your functions with an attribute and the AOP would substitute the function call by content of the function. Too bad Roslyn only support design time rewrites at the moment: http://stackoverflow.com/questions/7833954/can-i-use-roslyn-for-compile-time-code-rewriting – Guillaume86 Jan 09 '16 at 11:56
0

The problem is that your method becomes part of the expression tree and that EF cannot evaluate it. In principle, it is possible to evaluate parts of the expression tree before triggering the query. Have a look at Re-Linq: https://relinq.codeplex.com/ It has a class PartialEvaluatingExpressionTreeVisitor which can evaluate all partial expression trees, i.e. it will find your method, evaluate it, and inject the actual expression tree. This will come at a certain performance cost, but it may not be significant and you will have to gauge clean design versus performance.

Roland Buergi
  • 1,157
  • 9
  • 23