Comment convertir une chaîne en son arbre d'expression LINQ équivalent?

173

Il s'agit d'une version simplifiée du problème d'origine.

J'ai une classe appelée Person:

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

... et disons une instance:

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

Je voudrais écrire ce qui suit sous forme de chaîne dans mon éditeur de texte préféré ...

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

Je voudrais prendre cette chaîne et mon instance d'objet et évaluer un TRUE ou FALSE - c'est-à-dire évaluer un Func <Person, bool> sur l'instance d'objet.

Voici mes pensées actuelles:

  1. Implémentez une grammaire de base dans ANTLR pour prendre en charge la comparaison de base et les opérateurs logiques. Je pense à copier la priorité Visual Basic et certaines fonctionnalités ici: http://msdn.microsoft.com/en-us/library/fw84t893(VS.80).aspx
  2. Demandez à ANTLR de créer un AST approprié à partir d'une chaîne fournie.
  3. Parcourez l'AST et utilisez le framework Predicate Builder pour créer dynamiquement le Func <Person, bool>
  4. Évaluez le prédicat par rapport à une instance de Person comme requis

Ma question est: ai-je totalement surchargé cela? des alternatives?


EDIT: Solution choisie

J'ai décidé d'utiliser la bibliothèque Dynamic Linq, en particulier la classe Dynamic Query fournie dans LINQSamples.

Code ci-dessous:

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

Le résultat est de type System.Boolean, et dans ce cas est TRUE.

Un grand merci à Marc Gravell.

Inclure le package nuget System.Linq.Dynamic, documentation ici

Codebrain
la source
33
Merci d'avoir publié le code complet de la solution avec votre question. Très appréciée.
Adrian Grigore
Que faire si vous avez une collection ou des personnes et que vous souhaitez filtrer certains éléments? Personne.Age> 3 ET Personne.Poids> 50?
serhio
Merci. Je ne trouve pas DynamicExpression.ParseLambda (). Dans quel espace de noms et quel assembly se trouve-t-il?
Matt Fitzmaurice
Tout va bien. Il y avait une ambiguïté entre les espaces de noms. Nécessaire - en utilisant E = System.Linq.Expressions; en utilisant System.Linq.Dynamic;
Matt Fitzmaurice
Pourquoi utilise-t-il «ET» au lieu de «&&». N'est-ce pas censé être du code C #?
Triynko

Réponses:

65

La bibliothèque linq dynamique serait-elle utile ici? En particulier, je pense comme une Whereclause. Si nécessaire, placez-le dans une liste / un tableau juste pour l'appeler .Where(string)! c'est à dire

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

Sinon, écrire un analyseur (en utilisant Expressionsous le capot) n'est pas extrêmement éprouvant - j'en ai écrit un similaire (bien que je ne pense pas avoir la source) dans mon trajet en train juste avant Noël ...

Marc Gravell
la source
Marquez ce que vous entendez par «écrire un analyseur (en utilisant Expression sous le capot)» Analyser puis générer un arbre d'expression, ou System.Linq.Expressions a-t-il un mécanisme d'analyse?
AK_
Je suis presque sûr qu'il souhaite lire dans un fichier avec l'expression formée en tant que telle, puis le faire traduire en tant que prédicat et le compiler. La question semble être ,, obtenir la conversion de votre grammaire de «chaîne» en «prédicat». // 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 bon!
Latence du
31

Une autre bibliothèque de ce type est Flee

J'ai fait une comparaison rapide de Dynamic Linq Library et Flee and Flee était 10 fois plus rapide pour l'expression"(Name == \"Johan\" AND Salary > 500) OR (Name != \"Johan\" AND Salary > 300)"

Voici comment vous pouvez écrire votre code en utilisant 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
la source
Peut-être que je manque quelque chose, mais comment «fuir» aide-t-il à construire un arbre d'expression 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 a la Dump()méthode

Suneelsarraf
la source
où est la méthode GetProperty?
Alen.Toma
@ Alen.Toma J'ai dû changer le code pour var type = typeof(T); var prop = type.GetProperty(propName);le faire compiler.
Giles Roberts
Amit
5

Vous pouvez jeter un œil au DLR . Il vous permet d'évaluer et d'exécuter des scripts dans l'application .NET 2.0. Voici un exemple avec 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; }
}

Bien sûr, cette technique est basée sur l'évaluation d'exécution et le code ne peut pas être vérifié au moment de la compilation.

Darin Dimitrov
la source
1
Je veux être en mesure de me protéger contre l'exécution de «mauvais code» ... serait-ce une bonne solution?
Codebrain
Qu'entendez-vous par «mauvais code»? Quelqu'un qui tape une expression qui n'est pas valide? Dans ce cas, vous obtiendrez une exception d'exécution lors de la tentative d'évaluation du script.
Darin Dimitrov
@darin, des choses comme le démarrage de processus, la modification des données, etc.
sisve
2
'bad code' = quelque chose qui n'est pas une expression de type Func <Person, bool> (par exemple, supprimer des fichiers d'un disque, faire tourner un processus, etc ...)
Codebrain
1

Voici un exemple de combinateur d'analyseur basé sur Scala DSL pour l'analyse et l'évaluation d'expressions arithmétiques.

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'arbre d'expression équivalent ou l'arbre d'analyse de l'expression arithmétique fournie serait du type Parser [List [String]].

Plus de détails sont sur le lien suivant:

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

ncaralicea
la source
0

En plus de Dynamic Linq Library (qui construit une expression fortement typée et nécessite des variables fortement typées), je recommande une meilleure alternative: l'analyseur linq qui fait partie de NReco Commons Library (open source). Il aligne tous les types et effectue toutes les invocations au moment de l'exécution et se comporte comme un langage dynamique:

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
la source