Как использовать Func в запросе linq, который обеспечивает вывод IQueryable

Я предоставил следующий запрос (упрощенная версия), чтобы вернуть 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 Invoke не поддерживается в LINQ to Entities.

Сгенерированная ошибка, соответствующая решению (2):

Это ошибка компиляции: ожидаются методы, делегат или событие

Большое спасибо за любую расширенную помощь.


person Community    schedule 10.12.2017    source источник


Ответы (2)


Это действительно связано с дырявой абстракцией. выставлен IQueryable<> в сочетании с ORM.

Первая попытка повлияет на работу при выполнении в памяти; однако это не тот случай, когда используется ORM. причина, что ваш первый код не будет работать с LINQ to entity, в том, что Func<> является скомпилированным кодом. Он не представляет собой дерево выражений, которое можно легко преобразовать в SQL.

Вторая попытка - это естественное решение, но она не работает из-за несколько волшебного преобразования вашего кода в дерево выражений. Пока вы пишете select, вы не кодируете 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, что позволит вам встроить выражение прямо в дерево.

person Rob    schedule 11.12.2017
comment
У меня возникла проблема с вложенными встроенными вызовами, когда отдельная вспомогательная функция выражения использует другое подвыражение. В этом случае, я думаю, вы хотите, чтобы return res использовал другой ExpressionInliner для обработки вложенных операций. - person mellamokb; 25.09.2019
comment
Отлично работает, за исключением рекурсивных выражений, например: Expression ‹Func‹ int, bool ›› test = null; test = i = ›i‹ 0 || test.Inline (i - 1); какие-нибудь идеи обойти это? (всегда приводит к исключению stackoverflow) - person Isaac Abramowitz; 21.05.2021
comment
@IsaacAbramowitz К сожалению, это невозможно. Этот ответ только разворачивает вызов функции в дереве выражения. Поскольку он рекурсивен, нет возможности его должным образом расширить. Но даже в этом случае ... взять это выражение и преобразовать его в SQL было бы довольно сложно, вероятно, с участием CTE, которые я не уверен, что многие ORM даже поддерживают - person Rob; 26.05.2021
comment
@ Роб так не думал, но подумал, что спрошу, спасибо - person Isaac Abramowitz; 27.05.2021

Как я понимаю, это происходит потому, что IQueryable генерирует синтаксис SQL и запросы к базе данных точно так же, как типичная команда SQL. Он не может преобразовать функцию в команду SQL и, следовательно, не может быть выполнен таким образом. Вам нужно будет запросить наименьший объем данных, который вам нужен, принудительно выполнить запрос, чтобы у вас был результат, отличный от IQueryable, а затем снова запросить его с помощью функции func. Если функция требуется для предотвращения одновременного запроса слишком большого количества данных из БД, я предлагаю решить ее по-другому и или, возможно, добавить собственный метод БД. Есть способы обойти это, но суть в том, что IQueryable построен по-другому с намерением предотвратить множественные вызовы БД и сделать вызов максимально оптимизированным. Я не уверен, что вы умеете писать SQL; если вы это сделаете, то рассмотрите, что вызов IQueryable логически работает как SQL (потому что в основном это так). Надеюсь, это поможет.

person Michael Puckett II    schedule 10.12.2017
comment
Большое спасибо @Michael Puckett II за вашу помощь, но я должен сказать, что в этом случае я не могу использовать метод ToList () или другие методы семейства Enummerable, такие как ToArray () или т.д. - person ; 11.12.2017