Разбор логического дерева выражений с одним выражением

В документации MSDN есть хороший пример разбора дерева выражений:

// Create an expression tree.
Expression<Func<int, bool>> exprTree = num => num < 5;

// Decompose the expression tree.
ParameterExpression param = (ParameterExpression)exprTree.Parameters[0];
BinaryExpression operation = (BinaryExpression)exprTree.Body;
ParameterExpression left = (ParameterExpression)operation.Left;
ConstantExpression right = (ConstantExpression)operation.Right;

Console.WriteLine("Decomposed expression: {0} => {1} {2} {3}",
              param.Name, left.Name, operation.NodeType, right.Value);

Но я не видел примера того, как разобрать что-то вроде этого:

MyDomainObject foo;
Expression<Func<bool>> exprTree = () => ((foo.Scale < 5 || foo.Scale > 20) && foo.Scale <> -100) || foo.IsExempt;

Моя цель состоит в том, чтобы найти или создать утилиту, которая может (1) обрабатывать любой уровень вложенности скобок и (2) создавать строку, содержащую эквивалентное SQL-предложение «где», как результат разбора дерева выражений. У кого-нибудь есть фрагмент кода, который может помочь или знает пакет nuget, который решает эту проблему?

Для вложенного выражения, которое у меня есть выше, при условии, что таблица БД называется «MyDomainObject», правильный SQL, где вывод строки предложения, будет:

(( Scale < 5 or Scale > 20) and Scale != -100) or IsExempt = true

Очевидно, мой воображаемый синтаксический анализатор делает предположение, что в отсутствие бинарного оператора просто утверждается «true», как в случае «IsExempt = true».


person Brent Arias    schedule 02.03.2013    source источник
comment
Я просто хотел бы заявить, что вы можете игнорировать круглые скобки. Пункт в скобках просто указывает компилятору, должно ли 5 ​​+ 3 + 2 быть (5 + 3) + 2 или 5 + (3 + 2), однако компилятор представляет выражение в виде синтаксического дерева (примерно так: add(5, add(3, 2)) или add(add(5, 3), 2)).   -  person Alxandr    schedule 02.03.2013


Ответы (2)


Вот реализация, которая, по крайней мере, преобразует ваш ввод в допустимое выражение SQL. Вам нужно реализовать больше типов выражений самостоятельно, но это дает вам представление о том, как это работает.

Этот ответ очень похож на ответ Казецукай, но он использует Expression.NodeType для поиска операторов, поскольку в дереве выражений не будет MethodInfos.

Также имейте в виду, что это создает больше круглых скобок, чем на самом деле необходимо. Чтобы уменьшить количество круглых скобок, выражение необходимо проанализировать дополнительно с учетом приоритета операторов в SQL.

public static string GetSqlExpression(Expression expression)
{
    if (expression is BinaryExpression)
    {
        return string.Format("({0} {1} {2})",
            GetSqlExpression(((BinaryExpression)expression).Left),
            GetBinaryOperator((BinaryExpression)expression),
            GetSqlExpression(((BinaryExpression)expression).Right));
    }

    if (expression is MemberExpression)
    {
        MemberExpression member = (MemberExpression)expression;

        // it is somewhat naive to make a bool member into "Member = TRUE"
        // since the expression "Member == true" will turn into "(Member = TRUE) = TRUE"
        if (member.Type == typeof(bool))
        {
            return string.Format("([{0}] = TRUE)", member.Member.Name);
        }

        return string.Format("[{0}]", member.Member.Name);
    }

    if (expression is ConstantExpression)
    {
        ConstantExpression constant = (ConstantExpression)expression;

        // create a proper SQL representation for each type
        if (constant.Type == typeof(int) ||
            constant.Type == typeof(string))
        {
            return constant.Value.ToString();
        }

        if (constant.Type == typeof(bool))
        {
            return (bool)constant.Value ? "TRUE" : "FALSE";
        }

        throw new ArgumentException();
    }

    throw new ArgumentException();
}

public static string GetBinaryOperator(BinaryExpression expression)
{
    switch (expression.NodeType)
    {
        case ExpressionType.Equal:
            return "=";
        case ExpressionType.NotEqual:
            return "<>";
        case ExpressionType.OrElse:
            return "OR";
        case ExpressionType.AndAlso:
            return "AND";
        case ExpressionType.LessThan:
            return "<";
        case ExpressionType.GreaterThan:
            return ">";
        default:
            throw new ArgumentException();
    }
}

Результат:

(((([Scale] < 5) OR ([Scale] > 20)) AND ([Scale] <> -100)) OR ([IsExempt] = TRUE))

Вызовите метод следующим образом:

string sqlExpression = GetSqlExpression(exprTree.Body);

Я бы предложил построить дерево выражений более функциональным способом. Вместо того, чтобы строить Func<bool> из бетона foo, вы должны использовать Func<Foo, bool>. Тем не менее, это будет работать в любом случае. Это просто не выглядит правильно.

Expression<Func<Foo, bool>> exprTree =
    (foo) => ((foo.Scale < 5 || foo.Scale > 20) && foo.Scale != -100) || foo.IsExempt == true;

Очевидно, что обычно нет необходимости создавать текст SQL самостоятельно, если вы можете использовать LINQ to Entities. И LINQ для сущностей, и деревьев выражений требуют .NET 3.5, и вы действительно можете Перевести оператор LINQ в sql.

Я не уверен, что такое выражение, как IsExempt = TRUE, будет работать на SQL Server. Я думаю, что это должно быть IsExempt = 1, так как тип данных bit. Также такие выражения, как Value == null или Value != null, необходимо обрабатывать отдельно, поскольку вы не можете использовать Value = NULL или Value <> NULL в выражении SQL. Должно быть Value IS NULL или Value IS NOT NULL.

person pescolino    schedule 02.03.2013

Таким образом, похоже, что ваша проблема заключается не в «разборе» как таковом (поскольку выражение уже было проанализировано как выражение С#), вам нужно пройти по дереву выражений и вывести выражение SQL.

Я бы не рекомендовал использовать для этого собственный код, если вы можете этого избежать. Для большинства людей LINQ to Entities, вероятно, будет лучшим способом перейти , как это в основном и происходит. это для вас, полностью скрывая SQL.

Если у вас есть другие требования (например, более низкая версия .NET или у вас ДОЛЖНА быть строка SQL) и вы хотите написать код самостоятельно, вы можете сделать это с помощью рекурсивной функции. Эта функция может принимать выражение и возвращать предложение SQL в виде строки.

Что-то вроде строк (не проверял это, рассматривайте это как псевдокод):

public string WriteClause(Expression exp)
{
    if (exp is ParameterExpression)
    {
        return (exp as ParameterExpression).Name;
    }
    else if (exp is BinaryExpression)
    {
        var binaryExpression = exp as BinaryExpression;

        return "(" +
               WriteClause(binaryExpression.Left) + " "
               GetSqlOperator(binaryExpression.Method) + " "
               WriteClause(binaryExpression.Right) +
               ")";
    }
    else if...

    ...etc...

}

public string GetSqlOperator(MethodInfo method)
{
    switch (method.Name)
    {
        case "Add":
            return "+";
        case "Or":
            return "or";

        ...etc...
    }
}

Будучи рекурсивным, этот подход должен обрабатывать любой уровень глубины скобок. Это немного наивно, поэтому скобок будет больше, чем вам нужно.

person Kazetsukai    schedule 02.03.2013