How to use Func in a linq query that provide an IQueryable output
Asked Answered
P

2

5

I provided following query (simplified version) to return an IQueryable from my service:

var query =
            (from item in _entityRepository.DbSet()
             where
                 MyCondition
             orderby Entity.EntityID descending 
             select new DTOModel
             {
                 Id = Entity.EntityID,

                 ...,

                 //My problem is here, when I trying to call a function into linq query:
                 //Size = Entity.IsPersian ? (Entity.EntitySize.ConvertNumbersToPersian()) : (Entity.EntitySize)

                 //Solution (1):
                 //Size = ConvertMethod1(Entity)

                 //Solution (2):
                 //Size = ConvertMethod2(Entity)
             });

And also I have following codes in my service class according to my query:

//Corresponding to solution (1):
Func<Entity, string> ConvertMethod1 = p => (p.IsPersian ? p.EntitySize.ConvertNumbersToPersian() : p.EntitySize);

//Corresponding to solution (2):
Expression<Func<Entity, string>> ConvertMethod2 = (p) => (p.IsPersian ? p.EntitySize.ConvertNumbersToPersian() : p.EntitySize);

And I have seen this following errors:

Generated error corresponding to solution (1):

The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.

Generated error corresponding to solution (2):

Its a compilation error: Methods, delegate or event is expected

Thanks a lot for any advanced help.

Partible answered 10/12, 2017 at 23:40 Comment(0)
M
6

This is really down to the leaky abstraction exposed by IQueryable<> in combination with ORMs.

The first attempt will infact work when executing in memory; however, it's not the case when utilizing an ORM. The reason your first code won't work with LINQ to entities, is that a Func<> is compiled code. It doesn't represent an expression tree which can be easily converted to SQL.

The second attempt is the natural attempted solution, but breaks because of the somewhat magical conversion of your code into an expression tree. While you're writing the select, you're not coding against Expression objects. But when you compile the code; C# will automatically convert it into an expression tree. Unfortunately, there's no way to easily bring actual Expression items into the mix.

What you need is:

  1. A placeholder function to grab a reference to your expression
  2. An expression tree re-writer if you're going to send your query off to an ORM.

What you end up with your query is something like:

Expression<Func<Person, int>> personIdSelector = person => person.PersonID;

var query = Persons
    .Select(p =>
    new {
        a = personIdSelector.Inline(p)
    })
    .ApplyInlines();

With the following expression helpers:

public static class ExpressionExtensions
{
    public static TT Inline<T, TT>(this Expression<Func<T, TT>> expression, T item)
    {
        // This will only execute while run in memory.
        // LINQ to Entities / EntityFramework will never invoke this
        return expression.Compile()(item);
    }

    public static IQueryable<T> ApplyInlines<T>(this IQueryable<T> expression)
    {
        var finalExpression = expression.Expression.ApplyInlines().InlineInvokes();
        var transformedQuery = expression.Provider.CreateQuery<T>(finalExpression);
        return transformedQuery;
    }

    public static Expression ApplyInlines(this Expression expression) {
        return new ExpressionInliner().Visit(expression);
    }

    private class ExpressionInliner : ExpressionVisitor
    {
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.Name == "Inline" && node.Method.DeclaringType == typeof(ExpressionExtensions))
            {
                var expressionValue = (Expression)Expression.Lambda(node.Arguments[0]).Compile().DynamicInvoke();
                var arg = node.Arguments[1];
                var res = Expression.Invoke(expressionValue, arg);
                return res;
            }
            return base.VisitMethodCall(node);
        }
    }
}

// https://codereview.stackexchange.com/questions/116530/in-lining-invocationexpressions/147357#147357
public static class ExpressionHelpers
{
    public static TExpressionType InlineInvokes<TExpressionType>(this TExpressionType expression)
        where TExpressionType : Expression
    {
        return (TExpressionType)new InvokeInliner().Inline(expression);
    }

    public static Expression InlineInvokes(this InvocationExpression expression)
    {
        return new InvokeInliner().Inline(expression);
    }

    public class InvokeInliner : ExpressionVisitor
    {
        private Stack<Dictionary<ParameterExpression, Expression>> _context = new Stack<Dictionary<ParameterExpression, Expression>>();
        public Expression Inline(Expression expression)
        {
            return Visit(expression);
        }

        protected override Expression VisitInvocation(InvocationExpression e)
        {
            var callingLambda = e.Expression as LambdaExpression;
            if (callingLambda == null)
                return base.VisitInvocation(e);
            var currentMapping = new Dictionary<ParameterExpression, Expression>();
            for (var i = 0; i < e.Arguments.Count; i++)
            {
                var argument = Visit(e.Arguments[i]);
                var parameter = callingLambda.Parameters[i];
                if (parameter != argument)
                    currentMapping.Add(parameter, argument);
            }
            if (_context.Count > 0)
            {
                var existingContext = _context.Peek();
                foreach (var kvp in existingContext)
                {
                    if (!currentMapping.ContainsKey(kvp.Key))
                        currentMapping[kvp.Key] = kvp.Value;
                }
            }

            _context.Push(currentMapping);
            var result = Visit(callingLambda.Body);
            _context.Pop();
            return result;
        }

        protected override Expression VisitParameter(ParameterExpression e)
        {
            if (_context.Count > 0)
            {
                var currentMapping = _context.Peek();
                if (currentMapping.ContainsKey(e))
                    return currentMapping[e];
            }
            return e;
        }
    }
}

This will allow you to re-write the expression tree before it ever gets to an ORM, allowing you to inline the expression directly into the tree.

Myel answered 11/12, 2017 at 1:41 Comment(4)
I had a problem with nested Inline calls, where a separate expression helper function uses another sub-expression. In that case, I think you want return res to use another ExpressionInliner to handle nested usages.Elbertine
Works perfectly with the exception of recursive expressions ex: Expression<Func<int, bool>> test = null; test = i => i < 0 || test.Inline(i - 1); any ideas of getting around that? (always results in a stackoverflow exception)Andrew
@IsaacAbramowitz That's not possible, unfortunately. This answer only unwraps the function call in the expression tree. Since it's recursive, there's no way to properly expand it. But even so.. taking that expression and translating it to SQL would be quite a task, likely involving CTEs which I'm not sure many ORMs even supportMyel
@Myel didn't think so but thought I'd ask, thanksAndrew
S
2

This is, as I understand it to be, because IQueryable generates SQL syntax and queries against the database just like a typical SQL command. It can't resolve the func into a SQL command and therefore it can't be accomplished that way. You would have to query the least amount of data you need, force the query so you have a non IQueryable result, and then query against it again using the func. If the func is required to prevent querying too much data at once from the DB then I suggest resolving it differently and or possibly adding a custom DB method. There are ways around it but the bottom line is IQueryable is built differently with the intention to prevent making multiple calls to the DB and to make the call as optimized as possible. I'm not sure if you know how to write SQL; if you do then consider the IQueryable call to work like SQL logically (because basically it is). Hopefully this helps.

Stillman answered 10/12, 2017 at 23:59 Comment(1)
Thanks a lot @Michael Puckett II for your help but I should say I can't use ToList() method or other Enummerable family methods like ToArray() or etc in this case.Partible

© 2022 - 2024 — McMap. All rights reserved.