¿Cómo convertir una cadena a su árbol de expresión LINQ equivalente?

173

Esta es una versión simplificada del problema original.

Tengo una clase llamada Persona:

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

... y digamos una instancia:

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

Me gustaría escribir lo siguiente como una cadena en mi editor de texto favorito ...

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

Me gustaría tomar esta cadena y mi instancia de objeto y evaluar un VERDADERO o FALSO, es decir, evaluar un Func <Person, bool> en la instancia del objeto.

Aquí están mis pensamientos actuales:

  1. Implemente una gramática básica en ANTLR para admitir operadores lógicos y de comparación básicos. Estoy pensando en copiar la precedencia de Visual Basic y algunas de las características aquí: http://msdn.microsoft.com/en-us/library/fw84t893(VS.80).aspx
  2. Haga que ANTLR cree un AST adecuado a partir de una cadena proporcionada.
  3. Recorre el AST y usa el Generador de predicados marco para crear dinámicamente el Func <Person, bool>
  4. Evaluar el predicado contra una instancia de Persona según sea necesario

Mi pregunta es ¿he superado totalmente esto? alguna alternativa?


EDITAR: Solución elegida

Decidí usar la Biblioteca Dynamic Linq, específicamente la clase Dynamic Query provista en LINQSamples.

Código a continuación:

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

El resultado es del tipo System.Boolean, y en este caso es VERDADERO.

Muchas gracias a Marc Gravell.

Incluya el paquete System.Linq.Dynamics nuget, documentación aquí

Codebrain
fuente
33
Gracias por publicar el código de solución completo junto con su pregunta. Muy apreciado.
Adrian Grigore
¿Qué pasa si tiene una colección o personas y desea filtrar algunos elementos? Persona Edad> 3 Y Persona Peso> 50?
serhio
Gracias. No puedo encontrar DynamicExpression.ParseLambda (). ¿En qué espacio de nombres y ensamblaje está?
Matt Fitzmaurice
Todo bien ... Había una ambigüedad entre los espacios de nombres. Necesario - usando E = System.Linq.Expressions; usando System.Linq.Dynamic;
Matt Fitzmaurice
¿Por qué usa 'AND' en lugar de '&&'? ¿No se supone que es el código C #?
Triynko

Respuestas:

65

¿ La biblioteca dinámica de linq ayudaría aquí? En particular, estoy pensando como una Wherecláusula. Si es necesario, ¡colóquelo dentro de una lista / matriz solo para llamarlo .Where(string)! es decir

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

Si no, escribir un analizador sintáctico (usando Expressiondebajo del capó) no es muy exigente: escribí uno similar (aunque no creo que tenga la fuente) en mi viaje en tren justo antes de Navidad ...

Marc Gravell
fuente
Marque a qué se refiere con "escribir un analizador sintáctico (usando Expression debajo del capó)" Analizando y luego generando un árbol de expresiones, o System.Linq.Expressions tiene algún mecanismo de análisis?
AK_
Estoy bastante seguro de que quiere leer un archivo con la expresión formada como tal y luego traducirlo como predicado y compilarlo. La pregunta parece ser: conseguir que su gramática se convierta de 'cadena' a 'predicado'. // 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 bueno!
Latencia
31

Otra biblioteca de este tipo es Flee

Hice una comparación rápida de Dynamic Linq Library y Flee and Flee fue 10 veces más rápido para la expresión"(Name == \"Johan\" AND Salary > 500) OR (Name != \"Johan\" AND Salary > 300)"

Así es como puedes escribir tu código 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();
}
chikak
fuente
Tal vez me falta algo, pero ¿cómo ayuda 'huir' en la construcción de un árbol de expresión 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 tiene el Dump()método

suneelsarraf
fuente
¿Dónde está el método GetProperty?
Alen.Toma
@ Alen.Toma Tuve que cambiar el código para var type = typeof(T); var prop = type.GetProperty(propName);que se compilara.
Giles Roberts
Lo hizo compilar y volcar una salida
Amit
5

Puede echar un vistazo a la DLR . Le permite evaluar y ejecutar scripts dentro de la aplicación .NET 2.0. Aquí hay una muestra 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; }
}

Por supuesto, esta técnica se basa en la evaluación del tiempo de ejecución y el código no se puede verificar en tiempo de compilación.

Darin Dimitrov
fuente
1
Quiero poder protegerme contra la ejecución de 'código incorrecto' ... ¿sería adecuado?
Codebrain
¿Qué quieres decir con "código incorrecto"? ¿Alguien escribiendo una expresión que no es válida? En este caso, obtendrá una excepción de tiempo de ejecución cuando intente evaluar el script.
Darin Dimitrov
@darin, cosas como iniciar procesos, cambiar datos, etc.
sisve 05 de
2
'código incorrecto' = algo que no es una expresión de tipo Func <Persona, bool> (por ejemplo, eliminar archivos de un disco, acelerar un proceso, etc.)
Codebrain 05 de
1

Aquí hay un ejemplo de un combinador de analizador basado en DSL Scala para analizar y evaluar expresiones aritméticas.

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

El árbol de expresión equivalente o el árbol de análisis de la expresión aritmética proporcionada sería del tipo Parser [List [String]].

Más detalles en el siguiente enlace:

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

ncaralicea
fuente
0

Además de Dynamic Linq Library (que construye expresiones fuertemente tipadas y requiere variables fuertemente tipadas), recomiendo una mejor alternativa: linq parser esa parte de NReco Commons Library (código abierto). Alinea todos los tipos y realiza todas las invocaciones en tiempo de ejecución y se comporta como un lenguaje dinámico:

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
fuente