Wie konvertiere ich einen String in seinen entsprechenden LINQ-Ausdrucksbaum?

173

Dies ist eine vereinfachte Version des ursprünglichen Problems.

Ich habe eine Klasse namens Person:

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

... und sagen wir eine Instanz:

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

Ich möchte Folgendes als Zeichenfolge in meinen bevorzugten Texteditor schreiben ...

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

Ich möchte diesen String und meine Objektinstanz nehmen und TRUE oder FALSE auswerten, dh eine Func <Person, bool> auf der Objektinstanz auswerten.

Hier sind meine aktuellen Gedanken:

  1. Implementieren Sie eine grundlegende Grammatik in ANTLR, um grundlegende Vergleichs- und logische Operatoren zu unterstützen. Ich denke darüber nach, die Visual Basic-Priorität und einige der hier aufgeführten Funktionen zu kopieren: http://msdn.microsoft.com/en-us/library/fw84t893(VS.80).aspx
  2. Lassen Sie ANTLR aus einer bereitgestellten Zeichenfolge einen geeigneten AST erstellen.
  3. Gehen Sie durch den AST und verwenden Sie das Predicate Builder- Framework, um die Func <Person, bool> dynamisch zu erstellen
  4. Bewerten Sie das Prädikat nach Bedarf anhand einer Instanz von Person

Meine Frage ist, habe ich das total überbacken? irgendwelche Alternativen?


BEARBEITEN: Gewählte Lösung

Ich habe mich für die Dynamic Linq Library entschieden, insbesondere für die in den LINQSamples bereitgestellte Dynamic Query-Klasse.

Code unten:

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

Das Ergebnis ist vom Typ System.Boolean und in diesem Fall TRUE.

Vielen Dank an Marc Gravell.

Fügen Sie System.Linq.Dynamic nuget Paket, Dokumentation hier

Codebrain
quelle
33
Vielen Dank, dass Sie den vollständigen Lösungscode zusammen mit Ihrer Frage veröffentlicht haben. Sehr geschätzt.
Adrian Grigore
Was ist, wenn Sie eine Sammlung oder Personen haben und einige Elemente filtern möchten? Person.Age> 3 UND Person.Weight> 50?
Serhio
Vielen Dank. Ich kann DynamicExpression.ParseLambda () nicht finden. In welchem ​​Namespace und in welcher Assembly befindet sich das?
Matt Fitzmaurice
Alles gut. Es gab eine Mehrdeutigkeit zwischen den Namespaces. Erforderlich - mit E = System.Linq.Expressions; using System.Linq.Dynamic;
Matt Fitzmaurice
Warum wird 'AND' anstelle von '&&' verwendet? Soll es nicht C # -Code sein?
Triynko

Antworten:

65

Würde die dynamische Linq-Bibliothek hier helfen? Insbesondere denke ich als WhereKlausel. Wenn nötig, legen Sie es in eine Liste / ein Array, um es aufzurufen .Where(string)! dh

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

Wenn nicht, ist das Schreiben eines Parsers ( Expressionunter der Haube) nicht sehr anstrengend - ich habe kurz vor Weihnachten einen ähnlichen Parser ( obwohl ich nicht glaube, dass ich die Quelle habe) in meinem Zugverkehr geschrieben ...

Marc Gravell
quelle
Markieren Sie, was Sie mit "Parser schreiben (mit Ausdruck unter der Haube)" analysieren und dann einen Ausdrucksbaum generieren, oder verfügt System.Linq.Expressions über einen Analysemechanismus?
AK_
Ich bin mir ziemlich sicher, dass er eine Datei mit dem als solchen gebildeten Ausdruck einlesen und dann als Prädikat übersetzen und kompilieren lassen möchte. Die Frage scheint zu sein, ob Ihre Grammatik von "Zeichenfolge" in "Prädikat" konvertiert werden soll. // 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 gut!
Latenz
31

Eine andere solche Bibliothek ist Flee

Ich habe einen schnellen Vergleich von Dynamic Linq Library und Flee durchgeführt und Flee war zehnmal schneller für den Ausdruck"(Name == \"Johan\" AND Salary > 500) OR (Name != \"Johan\" AND Salary > 300)"

So können Sie Ihren Code mit Flee schreiben.

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();
}
Chikak
quelle
Vielleicht fehlt mir etwas, aber wie hilft "fliehen" beim Erstellen eines Linq-Ausdrucksbaums?
Michael B Hildebrand
9
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 hat die Dump()Methode

suneelsarraf
quelle
Wo ist die GetProperty-Methode?
Alen.Toma
@ Alen.Toma Ich musste den Code ändern var type = typeof(T); var prop = type.GetProperty(propName);, um ihn kompilieren zu können.
Giles Roberts
Hat es kompiliert und eine Ausgabe ausgegeben
Amit
5

Sie können sich das DLR ansehen . Sie können damit Skripts in der .NET 2.0-Anwendung auswerten und ausführen. Hier ist ein Beispiel mit 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; }
}

Natürlich basiert diese Technik auf der Laufzeitauswertung und der Code kann zur Kompilierungszeit nicht überprüft werden.

Darin Dimitrov
quelle
1
Ich möchte in der Lage sein, mich vor der Ausführung von "schlechtem Code" zu schützen ... würde dies gut passen?
Codebrain
Was meinst du mit "schlechtem Code"? Jemand, der einen ungültigen Ausdruck eingibt? In diesem Fall wird beim Versuch, das Skript auszuwerten, eine Laufzeitausnahme angezeigt.
Darin Dimitrov
@ Darin, Dinge wie Starten von Prozessen, Ändern von Daten usw.
Schwester
2
'bad code' = etwas, das kein Ausdruck vom Typ Func <Person, bool> ist (z. B. Löschen von Dateien von einer Festplatte,
Hochfahren
1

Hier ist ein Beispiel eines Scala DSL-basierten Parser-Kombinators zum Parsen und Auswerten von arithmetischen Ausdrücken.

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

Der äquivalente Ausdrucksbaum oder Analysebaum des angegebenen arithmetischen Ausdrucks wäre vom Typ Parser [List [String]].

Weitere Details finden Sie unter folgendem Link:

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

ncaralicea
quelle
0

Zusätzlich zur Dynamic Linq Library (die stark typisierte Ausdrücke erstellt und stark typisierte Variablen erfordert) empfehle ich eine bessere Alternative: Linq-Parser für diesen Teil der NReco Commons Library (Open Source). Es richtet alle Typen aus und führt alle Aufrufe zur Laufzeit aus und verhält sich wie eine dynamische Sprache:

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
Vitaliy Fedorchenko
quelle