【问题标题】:How to use Func in a linq query that provide an IQueryable output如何在提供 IQueryable 输出的 linq 查询中使用 Func
【发布时间】:2017-12-10 23:40:46
【问题描述】:

我提供了以下查询(简化版)以从我的服务返回 IQueryable:

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)
             });

根据我的查询,我的服务类中还有以下代码:

//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);

我看到了以下错误:

解决方案(1)对应的生成错误:

LINQ to Entities 不支持 LINQ 表达式节点类型“Invoke”。

解决方案(2)对应的生成错误:

这是一个编译错误: 需要方法、委托或事件

非常感谢任何高级帮助。

【问题讨论】:

    标签: c# linq iqueryable func


    【解决方案1】:

    这实际上取决于IQueryable&lt;&gt; 结合ORM 暴露的leaky abstraction

    在内存中执行时,第一次尝试实际上会起作用;但是,在使用 ORM 时情况并非如此。 原因您的第一个代码不适用于 LINQ to 实体,Func&lt;&gt;编译代码。它并不代表可以轻松转换为 SQL 的表达式树。

    第二次尝试是自然尝试的解决方案,但由于将代码转换为表达式树有点神奇而中断。在编写选择时,您没有针对 Expression 对象进行编码。但是当你编译代码时; C# 会自动将其转换为表达式树。不幸的是,没有办法轻松地将实际 Expression 项目混入其中。

    你需要的是:

    1. 一个占位符函数,用于获取对表达式的引用
    2. 表达式树重写器,如果您要将查询发送到 ORM。

    您最终得到的查询类似于:

    Expression<Func<Person, int>> personIdSelector = person => person.PersonID;
    
    var query = Persons
        .Select(p =>
        new {
            a = personIdSelector.Inline(p)
        })
        .ApplyInlines();
    

    使用以下表达式助手:

    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;
            }
        }
    }
    

    这将允许您在 到达 ORM 之前重写表达式树,从而允许您将表达式直接内联到树中。

    【讨论】:

    • 我遇到了嵌套内联调用的问题,其中一个单独的表达式辅助函数使用另一个子表达式。在这种情况下,我认为您希望 return res 使用另一个 ExpressionInliner 来处理嵌套用法。
    • 除了递归表达式 ex: Expression> test = null;测试 = i => i
    • @IsaacAbramowitz 不幸的是,这是不可能的。此答案仅在表达式树中展开函数调用。由于它是递归的,因此无法正确扩展它。但即便如此.. 将该表达式转换为 SQL 将是一项艰巨的任务,可能涉及 CTE,我不确定许多 ORM 是否支持
    • @Rob 不这么认为,但我想我会问的,谢谢
    【解决方案2】:

    据我了解,这是因为 IQueryable 生成 SQL 语法和对数据库的查询,就像典型的 SQL 命令一样。它无法将 func 解析为 SQL 命令,因此无法以这种方式完成。您必须查询所需的最少数据量,强制查询以便获得非 IQueryable 结果,然后使用 func 再次对其进行查询。如果需要 func 来防止一次从数据库查询太多数据,那么我建议以不同的方式解决它,或者可能添加一个自定义数据库方法。有一些方法可以解决它,但底线是 IQueryable 的构建方式不同,目的是防止对数据库进行多次调用并尽可能优化调用。我不确定你是否知道如何编写 SQL;如果你这样做了,那么考虑 IQueryable 调用在逻辑上像 SQL 一样工作(因为基本上是这样)。希望这会有所帮助。

    【讨论】:

    • 非常感谢@Michael Puckett II 的帮助,但我应该说在这种情况下我不能使用 ToList() 方法或其他 Enummerable 系列方法,如 ToArray() 等。
    猜你喜欢
    • 2023-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多