Come convertire una stringa nel suo albero delle espressioni LINQ equivalente?


173

Questa è una versione semplificata del problema originale.

Ho una classe chiamata Persona:

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

... e diciamo un'istanza:

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

Vorrei scrivere quanto segue come stringa nel mio editor di testo preferito ....

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

Vorrei prendere questa stringa e la mia istanza di oggetto e valutare un VERO o FALSO, ovvero valutare un Func <Person, bool> sull'istanza di oggetto.

Ecco i miei pensieri attuali:

  1. Implementare una grammatica di base in ANTLR per supportare il confronto di base e gli operatori logici. Sto pensando di copiare la precedenza di Visual Basic e alcune delle funzionalità qui: http://msdn.microsoft.com/en-us/library/fw84t893(VS.80).aspx
  2. Chiedi a ANTLR di creare un AST adatto da una stringa fornita.
  3. Cammina sull'AST e usa il framework Predicate Builder per creare dinamicamente Func <Person, bool>
  4. Valuta il predicato rispetto a un'istanza di Persona come richiesto

La mia domanda è: ho completamente esagerato? qualche alternativa?


EDIT: soluzione scelta

Ho deciso di utilizzare la Dynamic Linq Library, in particolare la classe Dynamic Query fornita nei LINQSamples.

Codice sotto:

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

Il risultato è di tipo System.Boolean e in questo caso è TRUE.

Mille grazie a Marc Gravell.

Includere il pacchetto nuget System.Linq.Dynamic, documentazione qui


33
Grazie per aver pubblicato il codice completo della soluzione insieme alla tua domanda. Molto apprezzato.
Adrian Grigore,

cosa succede se hai una collezione o persone e vorresti filtrare alcuni elementi? Person.Age> 3 AND Person.Weight> 50?
serhio,

Grazie. Non riesco a trovare DynamicExpression.ParseLambda (). In quale spazio dei nomi e assembly si trova?
Matt Fitzmaurice,

Tutto bene .. C'era un'ambiguità tra gli spazi dei nomi. Necessario: utilizzando E = System.Linq.Expressions; utilizzando System.Linq.Dynamic;
Matt Fitzmaurice,

Perché usa "AND" invece di "&&". Non dovrebbe essere un codice C #?
Triynko,

Risposte:


65

Sarebbe la dinamica LINQ libreria di aiuto qui? In particolare, sto pensando come una Whereclausola. Se necessario, inseriscilo in un elenco / array solo per richiamarlo .Where(string)! vale a dire

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

Altrimenti, scrivere un parser (usando Expressionsotto il cofano) non è molto faticoso - ne ho scritto uno simile (anche se non credo di avere la fonte) nel mio viaggio in treno poco prima di Natale ...


Segna cosa intendi per "scrivere un parser (usando Expression sotto il cofano)" Analizzare e quindi generare un albero di espressioni, oppure System.Linq.Expressions ha un meccanismo di analisi?
AK_

Sono abbastanza sicuro che voglia leggere in un file con l'espressione formata come tale e poi tradurlo come predicato e compilato. La domanda sembra essere: ottenere la conversione della grammatica da "stringa" a "predicato". // 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 bene!
Latenza

31

Un'altra libreria del genere è Flee

Ho fatto un rapido confronto tra Dynamic Linq Library e Flee and Flee è stato 10 volte più veloce per l'espressione"(Name == \"Johan\" AND Salary > 500) OR (Name != \"Johan\" AND Salary > 300)"

Ecco come puoi scrivere il tuo codice usando 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();
}

Forse mi manca qualcosa, ma in che modo 'fuggire' aiuta a costruire un albero di espressioni linq?
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 ha il Dump()metodo


dov'è il metodo GetProperty?
Alen.Toma

@ Alen.Toma Ho dovuto cambiare il codice per var type = typeof(T); var prop = type.GetProperty(propName);per farlo compilare.
Giles Roberts,

Ha reso compilare e discarica fuori un'uscita
Amit

5

Potresti dare un'occhiata al DLR . Ti consente di valutare ed eseguire script all'interno dell'applicazione .NET 2.0. Ecco un esempio con 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; }
}

Naturalmente questa tecnica si basa sulla valutazione del runtime e il codice non può essere verificato al momento della compilazione.


1
Voglio essere in grado di proteggere dall'esecuzione di un "codice errato" ... sarebbe una buona scelta?
Codebrain,

Cosa intendi con "codice errato"? Qualcuno sta digitando un'espressione che non è valida? In questo caso, si otterrà un'eccezione di runtime quando si tenta di valutare lo script.
Darin Dimitrov,

@darin, cose come l'avvio dei processi, la modifica dei dati, ecc.
sisve

2
'bad code' = qualcosa che non è un'espressione di tipo Func <Person, bool> (es. cancellare file da un disco, avviare un processo ecc ...)
Codebrain

1

Ecco un esempio di un combinatore parser basato su DSL Scala per l'analisi e la valutazione delle espressioni aritmetiche.

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

L'albero di espressione equivalente o l'albero di analisi dell'espressione aritmetica fornita sarebbe del tipo Parser [List [String]].

Maggiori dettagli sono al seguente link:

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


0

Oltre alla Dynamic Linq Library (che costruisce espressioni fortemente tipizzate e richiede variabili fortemente tipizzate), raccomando un'alternativa migliore: parser linq quella parte della NReco Commons Library (open source). Allinea tutti i tipi ed esegue tutte le invocazioni in fase di esecuzione e si comporta come un linguaggio dinamico:

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.