Как преобразовать строку в эквивалентное ей дерево выражений LINQ?

Это упрощенная версия исходной задачи.

У меня есть класс под названием Person:

public class Person {
  public string Name { get; set; }
  public int Age { get; set; }
  public int Weight { get; set; }
  public DateTime FavouriteDay { get; set; }
}

... и, скажем, пример:

var bob = new Person {
  Name = "Bob",
  Age = 30,
  Weight = 213,
  FavouriteDay = '1/1/2000'
}

Я хотел бы написать следующее как строку в моем любимом текстовом редакторе ....

(Person.Age > 3 AND Person.Weight > 50) OR Person.Age < 3

Я хотел бы взять эту строку и мой экземпляр объекта и оценить ИСТИНА или ЛОЖЬ, то есть оценить Func ‹Person, bool> в экземпляре объекта.

Вот мои текущие мысли:

  1. Реализуйте базовую грамматику в ANTLR для поддержки базовых операторов сравнения и логических операторов. Я подумываю скопировать приоритет Visual Basic и некоторые функции здесь: http://msdn.microsoft.com/en-us/library/fw84t893(VS.80).aspx
  2. Попросите ANTLR создать подходящий AST из предоставленной строки.
  3. Пройдите по AST и используйте структуру Predicate Builder для динамического создания Func ‹Person, bool>
  4. При необходимости оцените предикат для экземпляра Person

У меня вопрос: не перепекли ли я это полностью? какие-нибудь альтернативы?


РЕДАКТИРОВАТЬ: выбранное решение

Я решил использовать динамическую библиотеку Linq, в частности класс динамических запросов, представленный в LINQSamples.

Код ниже:

using System;
using System.Linq.Expressions;
using System.Linq.Dynamic;

namespace ExpressionParser
{
  class Program
  {
    public class Person
    {
      public string Name { get; set; }
      public int Age { get; set; }
      public int Weight { get; set; }
      public DateTime FavouriteDay { get; set; }
    }

    static void Main()
    {
      const string exp = @"(Person.Age > 3 AND Person.Weight > 50) OR Person.Age < 3";
      var p = Expression.Parameter(typeof(Person), "Person");
      var e = System.Linq.Dynamic.DynamicExpression.ParseLambda(new[] { p }, null, exp);
      var bob = new Person
      {
        Name = "Bob",
        Age = 30,
        Weight = 213,
        FavouriteDay = new DateTime(2000,1,1)
      };

      var result = e.Compile().DynamicInvoke(bob);
      Console.WriteLine(result);
      Console.ReadKey();
    }
  }
}

Результат имеет тип System.Boolean, в данном случае - ИСТИНА.

Большое спасибо Марку Гравеллу.

Включите пакет Nuget System.Linq.Dynamic, документацию здесь


person Codebrain    schedule 04.05.2009    source источник
comment
Спасибо, что разместили полный код решения вместе с вашим вопросом. Очень признателен.   -  person Adrian Grigore    schedule 22.10.2010
comment
что, если у вас есть коллекция или люди, и вы хотите отфильтровать некоторые элементы? Person.Age ›3 И ​​Person.Weight› 50?   -  person serhio    schedule 12.08.2011
comment
Спасибо. Я не могу найти DynamicExpression.ParseLambda (). В каком пространстве имен и сборке он находится?   -  person Matt Fitzmaurice    schedule 17.04.2015
comment
Все хорошо .. Между пространствами имен возникла двусмысленность. Необходимо - используя E = System.Linq.Expressions; using System.Linq.Dynamic;   -  person Matt Fitzmaurice    schedule 17.04.2015
comment
Почему используется «И» вместо «&&». Разве это не должен быть код C #?   -  person Triynko    schedule 28.01.2016
comment
Возможно, более производительным будет приведение делегата к Func ‹Person, bool› и использование Invoke вместо DynamicInvoke. См. Здесь: stackoverflow.com/questions/12858340/   -  person Doron Yaacoby    schedule 09.02.2016
comment
Чтобы это работало, вы загрузили динамическую библиотеку запросов C # (входит в каталог \ LinqSamples \ DynamicQuery), перейдите к CSharpSamples\LinqSamples\DynamicQuery\DynamicQuery и затем либо 1) скопируйте класс dynamic.cs из образца проекта в свой проект, либо 2) поместите dynamic.cs в свой собственный проект как отдельную dll с именем файла и пространством имен по умолчанию System.Linq.Dynamic (что я и сделал, потому что он более СУХИЙ.   -  person toddmo    schedule 04.03.2016
comment
Библиотека динамических запросов доступна в виде пакета NuGet под названием System.Linq.Dynamic.   -  person Zeus82    schedule 16.03.2016
comment
Мы будем очень признательны за ОБНОВЛЕНИЯ для .NET Core :-) :-) :-)   -  person Sentinel    schedule 24.10.2019


Ответы (7)


Будет ли здесь нужна динамическая библиотека linq? В частности, я имею в виду пункт Where. Если необходимо, поместите его в список / массив, чтобы вызвать .Where(string)! т.е.

var people = new List<Person> { person };
int match = people.Where(filter).Any();

Если нет, то написание синтаксического анализатора (с использованием Expression под капотом) не очень утомительно - я написал один похожий (хотя я не думаю, что у меня есть исходный код) в поездке на поезде незадолго до Рождества ...

person Marc Gravell    schedule 04.05.2009
comment
Отметьте, что вы имеете в виду, написав синтаксический анализатор (используя Expression под капотом) Parsing и затем генерируя дерево выражений, или у System.Linq.Expressions есть какой-то механизм синтаксического анализа? - person AK_; 01.07.2013
comment
Я почти уверен, что он хочет прочитать файл с выражением, сформированным как таковое, а затем перевести его как предикат и скомпилировать. Вопрос, похоже, заключается в том, чтобы преобразовать вашу грамматику из «строки» в «предикат». // Lambda expression as data in the form of an expression tree. System.Linq.Expressions.Expression<Func<int, bool>> expr = i => i < 5; // Compile the expression tree into executable code. Func<int, bool> deleg = expr.Compile(); // Invoke the method and print the output. Console.WriteLine("deleg(4) = {0}", deleg(4)); ParseLambda хорошо! - person Latency; 06.10.2017
comment
это жемчужина короны. Я гуглил и гуглил и пробовал 6 различных решений с открытым исходным кодом, потраченных впустую несколько часов - ни одно не работало без «но» и «если», и размер каждой DLL был в несколько раз больше, чем динамический LINQ .. и LINQ я заставил себя работать примерно за 20 минут. - person Boppity Bop; 28.12.2020

Еще одна такая библиотека - Flee.

Я быстро сравнил динамическую библиотеку Linq и « Убегать », и« бегство »было в 10 раз быстрее для выражения "(Name == \"Johan\" AND Salary > 500) OR (Name != \"Johan\" AND Salary > 300)".

Вот как вы можете написать свой код с помощью Flee.

static void Main(string[] args)
{
  var context = new ExpressionContext();
  const string exp = @"(Person.Age > 3 AND Person.Weight > 50) OR Person.Age < 3";
  context.Variables.DefineVariable("Person", typeof(Person));
  var e = context.CompileDynamic(exp);

  var bob = new Person
  {
    Name = "Bob",
    Age = 30,
    Weight = 213,
    FavouriteDay = new DateTime(2000, 1, 1)
  };

  context.Variables["Person"] = bob;
  var result = e.Evaluate();
  Console.WriteLine(result);
  Console.ReadKey();
}
person chikak    schedule 08.02.2010
comment
Может быть, я что-то упускаю, но как 'flee' помогает в построении дерева выражений linq? - person Michael B Hildebrand; 10.01.2020

void Main()
{
    var testdata = new List<Ownr> {
        //new Ownr{Name = "abc", Qty = 20}, // uncomment this to see it getting filtered out
        new Ownr{Name = "abc", Qty = 2},
        new Ownr{Name = "abcd", Qty = 11},
        new Ownr{Name = "xyz", Qty = 40},
        new Ownr{Name = "ok", Qty = 5},
    };

    Expression<Func<Ownr, bool>> func = Extentions.strToFunc<Ownr>("Qty", "<=", "10");
    func = Extentions.strToFunc<Ownr>("Name", "==", "abc", func);

    var result = testdata.Where(func.ExpressionToFunc()).ToList();

    result.Dump();
}

public class Ownr
{
    public string Name { get; set; }
    public int Qty { get; set; }
}

public static class Extentions
{
    public static Expression<Func<T, bool>> strToFunc<T>(string propName, string opr, string value, Expression<Func<T, bool>> expr = null)
    {
        Expression<Func<T, bool>> func = null;
        try
        {
            var type = typeof(T);
            var prop = type.GetProperty(propName);
            ParameterExpression tpe = Expression.Parameter(typeof(T));
            Expression left = Expression.Property(tpe, prop);
            Expression right = Expression.Convert(ToExprConstant(prop, value), prop.PropertyType);
            Expression<Func<T, bool>> innerExpr = Expression.Lambda<Func<T, bool>>(ApplyFilter(opr, left, right), tpe);
            if (expr != null)
                innerExpr = innerExpr.And(expr);
            func = innerExpr;
        }
        catch (Exception ex)
        {
            ex.Dump();
        }

        return func;
    }
    private static Expression ToExprConstant(PropertyInfo prop, string value)
    {
        object val = null;

        try
        {
            switch (prop.Name)
            {
                case "System.Guid":
                    val = Guid.NewGuid();
                    break;
                default:
                    {
                        val = Convert.ChangeType(value, prop.PropertyType);
                        break;
                    }
            }
        }
        catch (Exception ex)
        {
            ex.Dump();
        }

        return Expression.Constant(val);
    }
    private static BinaryExpression ApplyFilter(string opr, Expression left, Expression right)
    {
        BinaryExpression InnerLambda = null;
        switch (opr)
        {
            case "==":
            case "=":
                InnerLambda = Expression.Equal(left, right);
                break;
            case "<":
                InnerLambda = Expression.LessThan(left, right);
                break;
            case ">":
                InnerLambda = Expression.GreaterThan(left, right);
                break;
            case ">=":
                InnerLambda = Expression.GreaterThanOrEqual(left, right);
                break;
            case "<=":
                InnerLambda = Expression.LessThanOrEqual(left, right);
                break;
            case "!=":
                InnerLambda = Expression.NotEqual(left, right);
                break;
            case "&&":
                InnerLambda = Expression.And(left, right);
                break;
            case "||":
                InnerLambda = Expression.Or(left, right);
                break;
        }
        return InnerLambda;
    }

    public static Expression<Func<T, TResult>> And<T, TResult>(this Expression<Func<T, TResult>> expr1, Expression<Func<T, TResult>> expr2)
    {
        var invokedExpr = Expression.Invoke(expr2, expr1.Parameters.Cast<Expression>());
        return Expression.Lambda<Func<T, TResult>>(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
    }

    public static Func<T, TResult> ExpressionToFunc<T, TResult>(this Expression<Func<T, TResult>> expr)
    {
        var res = expr.Compile();
        return res;
    }
}

LinqPad имеет метод Dump()

person suneelsarraf    schedule 01.11.2013
comment
где метод GetProperty? - person Alen.Toma; 18.09.2017
comment
@ Alen.Toma Мне пришлось сменить код на var type = typeof(T); var prop = type.GetProperty(propName);, чтобы он скомпилировался. - person Giles Roberts; 14.03.2018
comment
Скомпилировал и выгрузил вывод - person Amit; 09.04.2018

Вы можете взглянуть на DLR. Он позволяет вам оценивать и выполнять сценарии внутри приложения .NET 2.0. Вот пример с IronRuby:

using System;
using IronRuby;
using IronRuby.Runtime;
using Microsoft.Scripting.Hosting;

class App
{
    static void Main()
    {
        var setup = new ScriptRuntimeSetup();
        setup.LanguageSetups.Add(
            new LanguageSetup(
                typeof(RubyContext).AssemblyQualifiedName,
                "IronRuby",
                new[] { "IronRuby" },
                new[] { ".rb" }
            )
        );
        var runtime = new ScriptRuntime(setup);
        var engine = runtime.GetEngine("IronRuby");
        var ec = Ruby.GetExecutionContext(runtime);
        ec.DefineGlobalVariable("bob", new Person
        {
            Name = "Bob",
            Age = 30,
            Weight = 213,
            FavouriteDay = "1/1/2000"
        });
        var eval = engine.Execute<bool>(
            "return ($bob.Age > 3 && $bob.Weight > 50) || $bob.Age < 3"
        );
        Console.WriteLine(eval);

    }
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public int Weight { get; set; }
    public string FavouriteDay { get; set; }
}

Конечно, этот метод основан на оценке времени выполнения, и код не может быть проверен во время компиляции.

person Darin Dimitrov    schedule 04.05.2009
comment
Я хочу иметь возможность защитить себя от выполнения «плохого кода» ... подойдет ли это? - person Codebrain; 04.05.2009
comment
Что вы имеете в виду под «плохим кодом»? Кто-то вводит недопустимое выражение? В этом случае вы получите исключение во время выполнения при попытке оценить сценарий. - person Darin Dimitrov; 04.05.2009
comment
@darin, такие вещи, как запуск процессов, изменение данных и т. д. - person sisve; 05.05.2009
comment
'плохой код' = что-то, что не является выражением типа Func ‹Person, bool› (например, удаление файлов с диска, запуск процесса и т. д.) - person Codebrain; 05.05.2009

Вот пример комбинатора синтаксического анализатора на основе Scala DSL для синтаксического анализа и оценки арифметических выражений.

import scala.util.parsing.combinator._
/** 
* @author Nicolae Caralicea
* @version 1.0, 04/01/2013
*/
class Arithm extends JavaTokenParsers {
  def expr: Parser[List[String]] = term ~ rep(addTerm | minusTerm) ^^
    { case termValue ~ repValue => termValue ::: repValue.flatten }

  def addTerm: Parser[List[String]] = "+" ~ term ^^
    { case "+" ~ termValue => termValue ::: List("+") }

  def minusTerm: Parser[List[String]] = "-" ~ term ^^
    { case "-" ~ termValue => termValue ::: List("-") }

  def term: Parser[List[String]] = factor ~ rep(multiplyFactor | divideFactor) ^^
    {
      case factorValue1 ~ repfactor => factorValue1 ::: repfactor.flatten
    }

  def multiplyFactor: Parser[List[String]] = "*" ~ factor ^^
    { case "*" ~ factorValue => factorValue ::: List("*") }

  def divideFactor: Parser[List[String]] = "/" ~ factor ^^
    { case "/" ~ factorValue => factorValue ::: List("/") }

  def factor: Parser[List[String]] = floatingPointConstant | parantExpr

  def floatingPointConstant: Parser[List[String]] = floatingPointNumber ^^
    {
      case value => List[String](value)
    }

  def parantExpr: Parser[List[String]] = "(" ~ expr ~ ")" ^^
    {
      case "(" ~ exprValue ~ ")" => exprValue
    }

  def evaluateExpr(expression: String): Double = {
    val parseRes = parseAll(expr, expression)
    if (parseRes.successful) evaluatePostfix(parseRes.get)
    else throw new RuntimeException(parseRes.toString())
  }
  private def evaluatePostfix(postfixExpressionList: List[String]): Double = {
    import scala.collection.immutable.Stack

    def multiply(a: Double, b: Double) = a * b
    def divide(a: Double, b: Double) = a / b
    def add(a: Double, b: Double) = a + b
    def subtract(a: Double, b: Double) = a - b

    def executeOpOnStack(stack: Stack[Any], operation: (Double, Double) => Double): (Stack[Any], Double) = {
      val el1 = stack.top
      val updatedStack1 = stack.pop
      val el2 = updatedStack1.top
      val updatedStack2 = updatedStack1.pop
      val value = operation(el2.toString.toDouble, el1.toString.toDouble)
      (updatedStack2.push(operation(el2.toString.toDouble, el1.toString.toDouble)), value)
    }
    val initial: (Stack[Any], Double) = (Stack(), null.asInstanceOf[Double])
    val res = postfixExpressionList.foldLeft(initial)((computed, item) =>
      item match {
        case "*" => executeOpOnStack(computed._1, multiply)
        case "/" => executeOpOnStack(computed._1, divide)
        case "+" => executeOpOnStack(computed._1, add)
        case "-" => executeOpOnStack(computed._1, subtract)
        case other => (computed._1.push(other), computed._2)
      })
    res._2
  }
}

object TestArithmDSL {
  def main(args: Array[String]): Unit = {
    val arithm = new Arithm
    val actual = arithm.evaluateExpr("(12 + 4 * 6) * ((2 + 3 * ( 4 + 2 ) ) * ( 5 + 12 ))")
    val expected: Double = (12 + 4 * 6) * ((2 + 3 * ( 4 + 2 ) ) * ( 5 + 12 ))
    assert(actual == expected)
  }
}

Эквивалентное дерево выражений или дерево синтаксического анализа предоставленного арифметического выражения будет иметь тип Parser [List [String]].

Более подробная информация по следующей ссылке:

http://nicolaecaralicea.blogspot.ca/2013/04/scala-dsl-for-parsing-and-evaluating-of.html

person ncaralicea    schedule 01.05.2013

В дополнение к динамической библиотеке Linq (которая создает строго типизированные выражения и требует строго типизированных переменных) я рекомендую лучшую альтернативу: парсер linq, который является частью NReco Commons Library (с открытым исходным кодом). Он выравнивает все типы и выполняет все вызовы во время выполнения и ведет себя как динамический язык:

var lambdaParser = new NReco.LambdaParser();
var varContext = new Dictionary<string,object>();
varContext["one"] = 1M;
varContext["two"] = "2";

Console.WriteLine( lambdaParser.Eval("two>one && 0<one ? (1+8)/3+1*two : 0", varContext) ); // --> 5
person Vitaliy Fedorchenko    schedule 03.07.2014

Хотя это относительно старый пост - это код для построителя выражений: AnyService - ExpressionTreeBuilder Это модульные тесты: AnyService - модульные тесты ExpressionTreeBuilder

person Saturn Technologies    schedule 30.04.2020